一、概述 以前看到這樣一句話,語言只是工具,演算法才是程式設計的靈魂。的確,演算法在電腦科學中的地位真的很重要,在很多大公司的筆試面試中,演算法掌握程度的考察都占據了很大一部分。不管是為了面試還是自身編程能力的提升,花時間去研究常見的演算法還是很有必要的。下麵是自己對於演算法這部分的學習總結。 演算法簡介 算 ...
這篇文章是常見數據結構與演算法整理總結的下篇,上一篇主要是對常見的數據結構進行集中總結,這篇主要是總結一些常見的演算法相關內容,文章中如有錯誤,歡迎指出。 |
|
一、概述
以前看到這樣一句話,語言只是工具,演算法才是程式設計的靈魂。的確,演算法在電腦科學中的地位真的很重要,在很多大公司的筆試面試中,演算法掌握程度的考察都占據了很大一部分。不管是為了面試還是自身編程能力的提升,花時間去研究常見的演算法還是很有必要的。下麵是自己對於演算法這部分的學習總結。
演算法簡介
演算法是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,演算法代表著用系統的方法描述解決問題的策略機制。對於同一個問題的解決,可能會存在著不同的演算法,為了衡量一個演算法的優劣,提出了空間複雜度與時間複雜度這兩個概念。
時間複雜度
一般情況下,演算法中基本操作重覆執行的次數是問題規模n的某個函數f(n),演算法的時間度量記為 ** T(n) = O(f(n)) **,它表示隨問題規模n的增大,演算法執行時間的增長率和f(n)的增長率相同,稱作演算法的漸近時間複雜度,簡稱時間複雜度。這裡需要重點理解這個增長率。
舉個例子,看下麵3個代碼:
1、{++x;}
2、for(i = 1; i <= n; i++) { ++x; }
3、for(j = 1; j <= n; j++)
for(j = 1; j <= n; j++)
{ ++x; }
上述含有 ++x 操作的語句的頻度分別為1 、n 、n^2,
假設問題的規模擴大了n倍,3個代碼的增長率分別是1 、n 、n^2
它們的時間複雜度分別為O(1)、O(n )、O(n^2)
空間複雜度
空間複雜度是對一個演算法在運行過程中臨時占用存儲空間大小的量度,記做S(n)=O(f(n))。一個演算法的優劣主要從演算法的執行時間和所需要占用的存儲空間兩個方面衡量。
二、查找演算法
查找和排序是最基礎也是最重要的兩類演算法,熟練地掌握這兩類演算法,並能對這些演算法的性能進行分析很重要,這兩類演算法中主要包括二分查找、快速排序、歸併排序等等。
順序查找
順序查找又稱線性查找。它的過程為:從查找表的最後一個元素開始逐個與給定關鍵字比較,若某個記錄的關鍵字和給定值比較相等,則查找成功,否則,若直至第一個記錄,其關鍵字和給定值比較都不等,則表明表中沒有所查記錄查找不成功,它的缺點是效率低下。
二分查找
- 簡介
二分查找又稱折半查找,對於有序表來說,它的優點是比較次數少,查找速度快,平均性能好。
二分查找的基本思想是將n個元素分成大致相等的兩部分,取a[n/2]與x做比較,如果x=a[n/2],則找到x,演算法中止;如果x<a[n/2],則只要在數組a的左半部分繼續搜索x,如果x>a[n/2],則只要在數組a的右半部搜索x。
二分查找的時間複雜度為O(logn)
- 實現
//給定有序查找表array 二分查找給定的值data
//查找成功返回下標 查找失敗返回-1
static int funBinSearch(int[] array, int data) {
int low = 0;
int high = array.length - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (data == array[mid]) {
return mid;
} else if (data < array[mid]) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
三、排序演算法
排序是電腦程式設計中的一種重要操作,它的功能是將一個數據元素(或記錄)的任意序列,重新排列成一個按關鍵字有序的序列。下麵主要對一些常見的排序演算法做介紹,並分析它們的時空複雜度。
常見排序演算法常見排序演算法性能比較:
圖片來自網路上面這張表中有穩定性這一項,排序的穩定性是指如果在排序的序列中,存在前後相同的兩個元素的話,排序前和排序後他們的相對位置不發生變化。
下麵從冒泡排序開始逐一介紹。
冒泡排序
- 簡介
冒泡排序的基本思想是:設排序序列的記錄個數為n,進行n-1次遍歷,每次遍歷從開始位置依次往後比較前後相鄰元素,這樣較大的元素往後移,n-1次遍歷結束後,序列有序。
例如,對序列(3,2,1,5)進行排序的過程是:共進行3次遍歷,第1次遍歷時先比較3和2,交換,繼續比較3和1,交換,再比較3和5,不交換,這樣第1次遍歷結束,最大值5在最後的位置,得到序列(2,1,3,5)。第2次遍歷時先比較2和1,交換,繼續比較2和3,不交換,第2次遍歷結束時次大值3在倒數第2的位置,得到序列(1,2,3,5),第3次遍歷時,先比較1和2,不交換,得到最終有序序列(1,2,3,5)。
需要註意的是,如果在某次遍歷中沒有發生交換,那麼就不必進行下次遍歷,因為序列已經有序。
- 實現
// 冒泡排序 註意 flag 的作用
static void funBubbleSort(int[] array) {
boolean flag = true;
for (int i = 0; i < array.length - 1 && flag; i++) {
flag = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag = true;
}
}
}
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
- 分析
最佳情況下冒泡排序只需一次遍歷就能確定數組已經排好序,不需要進行下一次遍歷,所以最佳情況下,時間複雜度為** O(n) **。
最壞情況下冒泡排序需要n-1次遍歷,第一次遍歷需要比較n-1次,第二次遍歷需要n-2次,...,最後一次需要比較1次,最差情況下時間複雜度為** O(n^2) **。
簡單選擇排序
- 簡介
簡單選擇排序的思想是:設排序序列的記錄個數為n,進行n-1次選擇,每次在n-i+1(i = 1,2,...,n-1)個記錄中選擇關鍵字最小的記錄作為有效序列中的第i個記錄。
例如,排序序列(3,2,1,5)的過程是,進行3次選擇,第1次選擇在4個記錄中選擇最小的值為1,放在第1個位置,得到序列(1,3,2,5),第2次選擇從位置1開始的3個元素中選擇最小的值2放在第2個位置,得到有序序列(1,2,3,5),第3次選擇因為最小的值3已經在第3個位置不需要操作,最後得到有序序列(1,2,3,5)。
- 實現
static void funSelectionSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int mink = i;
// 每次從未排序數組中找到最小值的坐標
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[mink]) {
mink = j;
}
}
// 將最小值放在最前面
if (mink != i) {
int temp = array[mink];
array[mink] = array[i];
array[i] = temp;
}
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
- 分析
簡單選擇排序過程中需要進行的比較次數與初始狀態下待排序的記錄序列的排列情況** 無關。當i=1時,需進行n-1次比較;當i=2時,需進行n-2次比較;依次類推,共需要進行的比較次數是(n-1)+(n-2)+…+2+1=n(n-1)/2,即進行比較操作的時間複雜度為 O(n^2) ,進行移動操作的時間複雜度為 O(n) 。總的時間複雜度為 O(n^2) **。
最好情況下,即待排序記錄初始狀態就已經是正序排列了,則不需要移動記錄。最壞情況下,即待排序記錄初始狀態是按第一條記錄最大,之後的記錄從小到大順序排列,則需要移動記錄的次數最多為3(n-1)。
簡單選擇排序是不穩定排序。
直接插入排序
- 簡介
直接插入的思想是:是將一個記錄插入到已排好序的有序表中,從而得到一個新的、記錄數增1的有序表。
例如,排序序列(3,2,1,5)的過程是,初始時有序序列為(3),然後從位置1開始,先訪問到2,將2插入到3前面,得到有序序列(2,3),之後訪問1,找到合適的插入位置後得到有序序列(1,2,3),最後訪問5,得到最終有序序列(1,2,3,5).
- 實現
static void funDInsertSort(int[] array) {
int j;
for (int i = 1; i < array.length; i++) {
int temp = array[i];
j = i - 1;
while (j > -1 && temp < array[j]) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = temp;
}
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
}
- 分析
最好情況下,當待排序序列中記錄已經有序時,則需要n-1次比較,不需要移動,時間複雜度為** O(n) 。最差情況下,當待排序序列中所有記錄正好逆序時,則比較次數和移動次數都達到最大值,時間複雜度為 O(n^2) 。平均情況下,時間複雜度為 O(n^2) **。
希爾排序
希爾排序又稱“縮小增量排序”,它是基於直接插入排序的以下兩點性質而提出的一種改進:(1) 直接插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率。(2) 直接插入排序一般來說是低效的,因為插入排序每次只能將數據移動一位。點擊查看更多關於希爾排序的內容
歸併排序
- 簡介
歸併排序是分治法的一個典型應用,它的主要思想是:將待排序序列分為兩部分,對每部分遞歸地應用歸併排序,在兩部分都排好序後進行合併。
例如,排序序列(3,2,8,6,7,9,1,5)的過程是,先將序列分為兩部分,(3,2,8,6)和(7,9,1,5),然後對兩部分分別應用歸併排序,第1部分(3,2,8,6),第2部分(7,9,1,5),對兩個部分分別進行歸併排序,第1部分繼續分為(3,2)和(8,6),(3,2)繼續分為(3)和(2),(8,6)繼續分為(8)和(6),之後進行合併得到(2,3),(6,8),再合併得到(2,3,6,8),第2部分進行歸併排序得到(1,5,7,9),最後合併兩部分得到(1,2,3,5,6,7,8,9)。
- 實現
//歸併排序
static void funMergeSort(int[] array) {
if (array.length > 1) {
int length1 = array.length / 2;
int[] array1 = new int[length1];
System.arraycopy(array, 0, array1, 0, length1);
funMergeSort(array1);
int length2 = array.length - length1;
int[] array2 = new int[length2];
System.arraycopy(array, length1, array2, 0, length2);
funMergeSort(array2);
int[] datas = merge(array1, array2);
System.arraycopy(datas, 0, array, 0, array.length);
}
}
//合併兩個數組
static int[] merge(int[] list1, int[] list2) {
int[] list3 = new int[list1.length + list2.length];
int count1 = 0;
int count2 = 0;
int count3 = 0;
while (count1 < list1.length && count2 < list2.length) {
if (list1[count1] < list2[count2]) {
list3[count3++] = list1[count1++];
} else {
list3[count3++] = list2[count2++];
}
}
while (count1 < list1.length) {
list3[count3++] = list1[count1++];
}
while (count2 < list2.length) {
list3[count3++] = list2[count2++];
}
return list3;
}
- 分析
歸併排序的時間複雜度為O(nlogn),它是一種穩定的排序,java.util.Arrays類中的sort方法就是使用歸併排序的變體來實現的。
快速排序
- 簡介
快速排序的主要思想是:在待排序的序列中選擇一個稱為主元的元素,將數組分為兩部分,使得第一部分中的所有元素都小於或等於主元,而第二部分中的所有元素都大於主元,然後對兩部分遞歸地應用快速排序演算法。
- 實現
// 快速排序
static void funQuickSort(int[] mdata, int start, int end) {
if (end > start) {
int pivotIndex = quickSortPartition(mdata, start, end);
funQuickSort(mdata, start, pivotIndex - 1);
funQuickSort(mdata, pivotIndex + 1, end);
}
}
// 快速排序前的劃分
static int quickSortPartition(int[] list, int first, int last) {
int pivot = list[first];
int low = first + 1;
int high = last;
while (high > low) {
while (low <= high && list[low] <= pivot) {
low++;
}
while (low <= high && list[high] > pivot) {
high--;
}
if (high > low) {
int temp = list[high];
list[high] = list[low];
list[low] = temp;
}
}
while (high > first && list[high] >= pivot) {
high--;
}
if (pivot > list[high]) {
list[first] = list[high];
list[high] = pivot;
return high;
} else {
return first;
}
}
- 分析
在快速排序演算法中,比較關鍵的一個部分是主元的選擇。在最差情況下,劃分由n個元素構成的數組需要進行n次比較和n次移動,因此劃分需要的時間是O(n)。在最差情況下,每次主元會將數組劃分為一個大的子數組和一個空數組,這個大的子數組的規模是在上次劃分的子數組的規模上減1,這樣在最差情況下演算法需要(n-1)+(n-2)+...+1= ** O(n^2) **時間。
最佳情況下,每次主元將數組劃分為規模大致相等的兩部分,時間複雜度為** O(nlogn) **。
堆排序
- 簡介
在介紹堆排序之前首先需要瞭解堆的定義,n個關鍵字序列K1,K2,…,Kn稱為堆,當且僅當該序列滿足如下性質(簡稱為堆性質):(1) ki <= k(2i)且 ki <= k(2i+1) (1 ≤ i≤ n/2),當然,這是小根堆,大根堆則換成>=號。
如果將上面滿足堆性質的序列看成是一個完全二叉樹,則堆的含義表明,完全二叉樹中所有的非終端節點的值均不大於(或不小於)其左右孩子節點的值。
堆排序的主要思想是:給定一個待排序序列,首先經過一次調整,將序列構建成一個大頂堆,此時第一個元素是最大的元素,將其和序列的最後一個元素交換,然後對前n-1個元素調整為大頂堆,再將其第一個元素和末尾元素交換,這樣最後即可得到有序序列。
- 實現
//堆排序
public class TestHeapSort {
public static void main(String[] args) {
int arr[] = { 5, 6, 1, 0, 2, 9 };
heapsort(arr, 6);
System.out.println(Arrays.toString(arr));
}
static void heapsort(int arr[], int n) {
// 先建大頂堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapAdjust(arr, i, n);
}
for (int i = 0; i < n - 1; i++) {
swap(arr, 0, n - i - 1);
heapAdjust(arr, 0, n - i - 1);
}
}
// 交換兩個數
static void swap(int arr[], int low, int high) {
int temp = arr[low];
arr[low] = arr[high];
arr[high] = temp;
}
// 調整堆
static void heapAdjust(int arr[], int index, int n) {
int temp = arr[index];
int child = 0;
while (index * 2 + 1 < n) {
child = index * 2 + 1;
// child為左右孩子中較大的那個
if (child != n - 1 && arr[child] < arr[child + 1]) {
child++;
}
// 如果指定節點大於較大的孩子 不需要調整
if (temp > arr[child]) {
break;
} else {
// 否則繼續往下判斷孩子的孩子 直到找到合適的位置
arr[index] = arr[child];
index = child;
}
}
arr[index] = temp;
}
}
- 分析
由於建初始堆所需的比較次數較多,所以堆排序不適宜於記錄數較少的文件。堆排序時間複雜度也為O(nlogn),空間複雜度為O(1)。它是不穩定的排序方法。與快排和歸併排序相比,堆排序在最差情況下的時間複雜度優於快排,空間效率高於歸併排序。
四、其它演算法
在上面的篇幅中,主要是對查找和常見的幾種排序演算法作了介紹,這些內容都是基礎的但是必須掌握的內容,尤其是二分查找、快排、堆排、歸併排序這幾個更是面試高頻考察點。(這裡不禁想起百度一面的時候讓我寫二分查找和堆排序,二分查找還行,然而堆排序當時一臉懵逼...)下麵主要是介紹一些常見的其它演算法。
遞歸
- 簡介
在平常解決一些編程或者做一些演算法題的時候,經常會用到遞歸。程式調用自身的編程技巧稱為遞歸。它通常把一個大型複雜的問題層層轉化為一個與原問題相似的規模較小的問題來求解。上面介紹的快速排序和歸併排序都用到了遞歸的思想。
- 經典例子
斐波那契數列,又稱黃金分割數列、因數學家列昂納多·斐波那契以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:0、1、1、2、3、5、8、13、21、34、……在數學上,斐波納契數列以如下被以遞歸的方法定義:F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n≥2,n∈N*)。
//斐波那契數列 遞歸實現
static long funFib(long index) {
if (index == 0) {
return 0;
} else if (index == 1) {
return 1;
} else {
return funFib(index - 1) + funFib(index - 2);
}
}
上面代碼是斐波那契數列的遞歸實現,然而我們不難得到它的時間複雜度是O(2^n),遞歸有時候可以很方便地解決一些問題,但是它也會帶來一些效率上的問題。下麵的代碼是求斐波那契數列的另一種方式,效率比遞歸方法的效率高。
static long funFib2(long index) {
long f0 = 0;
long f1 = 1;
long f2 = 1;
if (index == 0) {
return f0;
} else if (index == 1) {
return f1;
} else if (index == 2) {
return f2;
}
for (int i = 3; i <= index; i++) {
f0 = f1;
f1 = f2;
f2 = f0 + f1;
}
return f2;
}
分治演算法
分治演算法的思想是將待解決的問題分解為幾個規模較小但類似於原問題的子問題,遞歸地求解這些子問題,然後合併這些子問題的解來建立最終的解。分治演算法中關鍵地一步其實就是遞歸地求解子問題。關於分治演算法的一個典型例子就是上面介紹的歸併排序。查看更多關於分治演算法的內容
動態規劃
動態規劃與分治方法相似,都是通過組合子問題的解來求解待解決的問題。但是,分治演算法將問題劃分為互不相交的子問題,遞歸地求解子問題,再將它們的解組合起來,而動態規劃應用於子問題重疊的情況,即不同的子問題具有公共的子子問題。動態規劃方法通常用來求解最優化問題。查看更多關於動態規劃的內容
動態規劃典型的一個例子是最長公共子序列問題。
常見的演算法還有很多,比如貪心演算法,回溯演算法等等,這裡都不再詳細介紹,想要熟練掌握,還是要靠刷題,刷題,刷題,然後總結。
五、常見演算法題
下麵是一些常見的演算法題彙總。
不使用臨時變數交換兩個數
static void funSwapTwo(int a, int b) {
a = a ^ b;
b = b ^ a;
a = a ^ b;
System.out.println(a + " " + b);
}
判斷一個數是否為素數
static boolean funIsPrime(int m) {
boolean flag = true;
if (m == 1) {
flag = false;
} else {
for (int i = 2; i <= Math.sqrt(m); i++) {
if (m % i == 0) {
flag = false;
break;
}
}
}
return flag;
}
其它演算法題
1、15道使用頻率極高的基礎演算法題
2、二叉樹相關演算法題
3、鏈表相關演算法題
4、字元串相關演算法問題
六、總結
以上就是自己對常見的演算法相關內容的總結,演算法虐我千百遍,我待演算法如初戀,革命尚未成功,同志仍需刷題,加油。
作者:塵語凡心
鏈接:https://www.jianshu.com/p/42f81846c0fb
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。