之前看到這樣一句話,語言只是工具,算法纔是程序設計的靈魂。的確,算法在計算機科學中的地位真的很重要,在不少大公司的筆試面試中,算法掌握程度的考察都佔據了很大一部分。無論是爲了面試仍是自身編程能力的提高,花時間去研究常見的算法仍是頗有必要的。下面是本身對於算法這部分的學習總結。html
算法簡介java
算法是指解題方案的準確而完整的描述,是一系列解決問題的清晰指令,算法表明着用系統的方法描述解決問題的策略機制。對於同一個問題的解決,可能會存在着不一樣的算法,爲了衡量一個算法的優劣,提出了空間複雜度與時間複雜度這兩個概念。es6
時間複雜度面試
通常狀況下,算法中基本操做重複執行的次數是問題規模n的某個函數f(n),算法的時間度量記爲 T(n) = O(f(n)) ,它表示隨問題規模n的增大,算法執行時間的增加率和f(n)的增加率相同,稱做算法的漸近時間複雜度,簡稱時間複雜度。這裏須要重點理解這個增加率。算法
舉個例子,看下面3個代碼: 一、{++x;} 二、for(i = 1; i <= n; i++) { ++x; } 三、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三、2一、3四、……在數學上,斐波納契數列以以下被以遞歸的方法定義: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; }
其它算法題