面試的死亡高發區是什麼?手寫快排.html
其餘的排序算法也常常會問到,雖然在工做中,咱們不多有須要本身手寫排序算法的機會,可是這種入門級的算法倒是證實咱們能力的一種簡單方法.所以要熟悉掌握.java
這篇文章,詳細記錄經常使用的一些排序算法,留以備忘.git
本文全部代碼可在github上下載查看.傳送門github
爲了方便本身寫,在測試過程當中,使用了策略模式,感興趣的童鞋能夠移步設計模式之-策略模式面試
圖片來自:www.cnblogs.com/guoyaohua/p…, 實在是懶得本身畫一遍了.算法
代碼測試排序數據:[26,13,3,5,27,36,42,2,4,44,34,25,59,58] 文內舉例測試數據:[5,2,4,3,1]設計模式
排序順序爲升序,即小數在前.數組
它重複地走訪過要排序的元素列,依次比較兩個相鄰的元素,若是他們的順序(如從大到小、首字母從A到Z)錯誤就把他們交換過來。走訪元素的工做是重複地進行直到沒有相鄰元素須要交換,也就是說該元素已經排序完成。數據結構
這個算法的名字由來是由於越大的元素會經由交換慢慢「浮」到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端同樣,故名「冒泡排序」。ide
public int[] sort(int[] input) {
for (int i = 0; i < input.length - 1; i++) {
for (int j = 0; j < input.length - i - 1; j++) {
if (input[j] > input[j + 1]) {
exchange(input, j, j + 1);
}
}
}
return input;
}
複製代碼
最佳狀況:T(n) = O(n)
最差狀況:T(n) = O(n2)
平均狀況:T(n) = O(n2)
冒泡排序是相鄰兩個交換,當相等的時候不會交換,所以能夠保證穩定性.
冒泡排序經過,相鄰元素的交換,能夠每次將最大的元素移到數組末尾.
兩層循環,第一層循環控制已經排序了多少位
,即末尾有多少個排序好的較大值.
第二層循環控制從0開始,逐次比較當前位置與下一位置,拿到當前最大值
,知道放在已經排序好的較大值序列前.
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,而後,再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。以此類推,直到所有待排序的數據元素排完。
public int[] sort(int[] input) {
int minIndex = 0;
for (int i = 0; i < input.length; i++) {
minIndex = i; // 將當前位置做爲最小值得下標
for (int j = i; j < input.length; j++) {
if (input[minIndex] > input[j]) {
//若是發現比最小下標的值還小的位置,替換最小下標
minIndex = j;
}
}
//將當前位置和最小下標位置的值交換
exchange(input, minIndex, i);
}
return input;
}
複製代碼
選擇排序表現十分穩定了能夠說,無論輸入是逆序仍是正序,他都須要對每個進行逐次比較,穩定的O(n2).
最佳狀況:T(n) = O(n2)
最差狀況:T(n) = O(n2)
平均狀況:T(n) = O(n2)
選擇排序不穩定
舉個例子,序列5 8 5 2 9,咱們知道第一遍選擇第1個元素5會和2交換,那麼原序列中兩個5的相對先後順序就被破壞了,因此選擇排序是一個不穩定的排序算法。
選擇排序其實能夠理解爲:用一個額外空間,一直記錄着當前最小的元素,第一遍結束後,該位置就是最小的,將第一個位置和該位置交換.
選擇排序的兩層循環,第一層循環控制當前序列的前多少位已經有序.
第二層循環控制從已經有序的下一位開始到結束,找到最小的
插入排序的基本思想是:每步將一個待排序的記錄,按其關鍵碼值的大小插入前面已經排序的文件中適當位置上,直到所有插入完爲止。
public int[] sort(int[] input) {
for (int i = 1; i < input.length; i++) {
int j = i - 1;
//拿到當前待插入的值
int current = input[i];
//從當前位置向前遍歷,逐一比較
while (j >= 0) {
//若是當前值大於該位置的值
if (current > input[j]) {
//在該位置以後放入當前值,跳出循環
input[j + 1] = current;
break;
} else {
//將該位置的值後移一位
input[j + 1] = input[j];
}
if (j == 0){
//若是該位置爲0,且大於當前值,則將當前值放在第一位
input[j] = current;
}
j--;
}
}
return input;
}
複製代碼
最佳狀況:T(n) = O(n)
最壞狀況:T(n) = O(n2)
平均狀況:T(n) = O(n2)
因爲是從後向前按照順序插入的,所以能夠保證穩定性.
舉個例子,序列5 8 5 2 9,在第三步時,將5插入5,8
的有序序列,會放在5以後,不會影響兩個5的相對位置.
插入排序也算是一種比較容易理解的排序方式.對一百個數字的排序能夠先排第一個.而後將第二個放入到已有的序列中,再將第三個放進來.
這樣的思路很好理解,代碼實現也較爲簡單,可是若是待排序序列是個反序的,即最壞狀況下,時間複雜度較高,只能用於少許數據的排序.
希爾排序(Shell's Sort)是插入排序的一種又稱「縮小增量排序」(Diminishing Increment Sort),是直接插入排序算法的一種更高效的改進版本。該方法因D.L.Shell於1959年提出而得名。
希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量減至1時,整個文件恰被分紅一組,算法便終止。 [1]
排序過程:先取一個正整數d1<n,把全部序號相隔d1的數組元素放一組,組內進行直接插入排序;而後取d2<d1,重複上述分組和排序操做;直至di=1,即全部記錄放進一個組中排序爲止。
public int[] sort(int[] input) {
//初始步長
int d = input.length / 2;
//當步長大於等於1,保證最後做爲一個數組排序過
while (d >= 1) {
//對每一種步長,遍歷全部(步長爲5,遍歷1到5,便可遍歷全部)
for (int i = 0; i < d; i++) {
//插入排序,普通插入排序每次遞增1,這裏遞增步長d
for (int j = i + d; j < input.length; j += d) {
int tmp = input[j];
int p;
for (p = j - d; p >= 0 && input[p] > tmp; p -= d) {
input[p + d] = input[p];
}
input[p + d] = tmp;
}
}
//步長減半
d=d/2;
}
return input;
}
複製代碼
最佳狀況:T(n) = O(nlog2 n)
最壞狀況:T(n) = O(nlog2 n)
平均狀況:T(n) =O(nlog2n)
不穩定.
雖然插入排序是穩定的,可是在分組的時候,可能致使兩個相同數字的相對順序有改變.
希爾排序就是一種優化後的插入排序,插入排序越到後面越麻煩,由於要移動的位置更多.
希爾排序能夠在開始的時候,經過分組,將待排序序列的"大體"順序變得好一些.這樣在最後合併爲一個分組時,能夠減小不少次的移動.以此來提高效率.
第一次分組,兩個元素爲一組,此時對每個組的插入排序來講都很簡單,所以只有兩個元素,可是對於序列的有序度
提高很是大.
歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。歸併排序是一種穩定的排序方法。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
public int[] sort(int[] input) {
//當長度小於2,返回
if (input.length < 2) {
return input;
}
//分隔成左右兩部分
int mid = input.length / 2;
int[] left = Arrays.copyOfRange(input, 0, mid);
int[] right = Arrays.copyOfRange(input, mid, input.length);
//分別進行歸併排序並merge結果
return merge(sort(left), sort(right));
}
public int[] merge(int[] A, int[] B) {
//定義新數組,長度等於兩個數組織和
int[] result = new int[A.length + B.length];
//定義三個指針,指向兩個輸入數組和結果數組
int i = 0, j = 0, h = 0;
//當A,B都沒有遍歷完的時候
while (i < A.length && j < B.length) {
//取較小的一個加入結果數組,而後將該數組的指針後移,結果數組指針後移
if (A[i] <= B[j]) {
result[h] = A[i];
i++;
} else {
result[h] = B[j];
j++;
}
h++;
}
//分別遍歷兩個數組,將剩餘數字加入結果數組中.
//這裏其實只會執行一個,由於從while循環中出來,必然有一個數組被遍歷完了.
for (; i < A.length; i++, h++) {
result[h] = A[i];
}
for (; j < B.length; j++, h++) {
result[h] = B[j];
}
//返回結果
return result;
}
複製代碼
最佳狀況:T(n) = O(n)
最差狀況:T(n) = O(nlogn)
平均狀況:T(n) = O(nlogn)
穩定.
在分隔的過程當中,不會影響穩定性.合併的過程當中,也不會影響.
歸併排序是分治思想的體現,先將帶排序數組分隔成兩半,而後分別進行歸併排序.最後將這兩部分合並起來.
其實至關於,將每一個元素做爲一個序列,不斷的進行兩個序列的合併過程,在合併的過程當中,保持了有序.
哈哈,這就是面試殺手,手寫快排了!
快速排序的基本思想:經過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另外一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。
public int[] sort(int[] input) {
quickSort(input, 0, input.length - 1);
return input;
}
private void quickSort(int[] a, int start, int end) {
if (start < end) {
//若是不止一個元素,繼續劃分兩邊遞歸排序下去
int partition = partition(a, start, end);
quickSort(a, start, partition - 1);
quickSort(a, partition + 1, end);
}
}
public int partition(int[] a, int start, int end) {
//以最左邊的值爲基準
int base = a[start];
//start一旦等於end,就說明左右兩個指針合併到了同一位置,能夠結束此輪循環。
while (start < end) {
while (start < end && a[end] >= base) {
//從右邊開始遍歷,若是比基準值大,就繼續向左走
end--;
}
//上面的while循環結束時,就說明當前的a[end]的值比基準值小,應與基準值進行交換
if (start < end) {
//交換
exchange(a, start, end);
//交換後,此時的那個被調換的值也同時調到了正確的位置(基準值左邊),所以左邊也要同時向後移動一位
start++;
}
while (start < end && a[start] <= base) {
//從左邊開始遍歷,若是比基準值小,就繼續向右走
start++;
}
//上面的while循環結束時,就說明當前的a[start]的值比基準值大,應與基準值進行交換
if (start < end) {
//交換
exchange(a, start, end);
//交換後,此時的那個被調換的值也同時調到了正確的位置(基準值右邊),所以右邊也要同時向前移動一位
end--;
}
}
//這裏返回start或者end皆可,此時的start和end都爲基準值所在的位置
return end;
}
複製代碼
最佳狀況:T(n) = O(nlogn)
最差狀況:T(n) = O(n2)
平均狀況:T(n) = O(nlogn)
不穩定.
27 23 27 3 以第一個27做爲pivot中心點,則27與後面那個3交換,造成 3 23 27 27,排序通過一次結束,但最後那個27在排序之初先於初始位置3那個27,因此不穩定。
快排真的是蠻麻煩的...
中心思想就是能夠指定一個基準位,比它大的放右邊,比它小的放左邊,而後對左右分別進行快排.
這個放的過程,每次看都能理解,可是總是記不住..
快排有個問題,基準的選擇對效率的影響很大,極限狀況下你每次都選最大的,效率就不好了.能夠經過隨機選取基準的方法略微的規避一下這個問題.
堆排序利用了堆這一數據結構,這裏只寫"堆排序"中關於"排序"的部分,對堆的詳細解釋在其餘文章中進行.
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。
int len;
@Override
public int[] sort(int[] input) {
len = input.length;
if (len < 1) {
return input;
}
//1.構建一個最大堆
buildMaxHeap(input);
//2.循環將堆首位(最大值)與末位交換,而後在從新調整最大堆
while (len > 0) {
exchange(input, 0, len - 1);
len--;
adjustHeap(input, 0);
}
return input;
}
/** * 創建最大堆 */
private void buildMaxHeap(int[] array) {
//從最後一個非葉子節點開始向上構造最大堆
for (int i = (len / 2 - 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
/** * 調整使之成爲最大堆 */
private void adjustHeap(int[] array, int i) {
int maxIndex = i;
//若是有左子樹,且左子樹大於父節點,則將最大指針指向左子樹
if (i * 2 < len && array[i * 2] > array[maxIndex]) {
maxIndex = i * 2;
}
//若是有右子樹,且右子樹大於父節點,則將最大指針指向右子樹
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex]) {
maxIndex = i * 2 + 1;
}
//若是父節點不是最大值,則將父節點與最大值交換,而且遞歸調整與父節點交換的位置。
if (maxIndex != i) {
exchange(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
複製代碼
最佳狀況:T(n) = O(nlogn)
最差狀況:T(n) = O(nlogn)
平均狀況:T(n) = O(nlogn)
不穩定.
堆排序的難點其實不在排序
上,而在與堆
上.]
如何構造一個最大(最小堆)?
移除堆頂元素後如何調整堆?
計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素是待排序數組A中值等於i的元素的個數。而後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。
做爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有肯定範圍的整數。
public int[] sort(int[] input) {
if (input.length == 0) {
return input;
}
//求出最大最小值
int bias, min = input[0], max = input[0];
for (int i = 1; i < input.length; i++) {
if (input[i] > max) {
max = input[i];
}
if (input[i] < min) {
min = input[i];
}
}
//最小值距離0的距離
bias = 0 - min;
//計數用的數組
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
//計數
for (int i = 0; i < input.length; i++) {
bucket[input[i] + bias]++;
}
//
int index = 0, i = 0;
while (index < input.length) {
if (bucket[i] != 0) {
//下標數字存在,注意放入結果中
input[index] = i - bias;
bucket[i]--;
index++;
} else {
//下標數字不存在,後移一位
i++;
}
}
return input;
}
複製代碼
最佳狀況:T(n) = O(n+k)
最差狀況:T(n) = O(n+k)
平均狀況:T(n) = O(n+k)
穩定
線性時間的排序方法,看起來真的很美好.可是限制太大了,主要有兩點.
必須是整數之間的排序
待排序序列的範圍不能太大,好比排序(1,2,3,4)就很好.若是排序(1,10000,10000000)就會佔用太大的內存.
桶排序 (Bucket sort)或所謂的箱排序,是一個排序算法,工做的原理是將數組分到有限數量的桶子裏。每一個桶子再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序)
public int[] sort(int[] input) {
//分桶,這裏採用映射函數f(x)=x/10。
//輸入數據爲0~99之間的數字
int bucketCount =10;
Integer[][] bucket = new Integer[bucketCount][input.length]; //Integer初始爲null,以與數字0區別。
for (int i=0; i<input.length; i++){
int quotient = input[i]/10; //這裏便是使用f(x)
for (int j=0; j<input.length; j++){
if (bucket[quotient][j]==null){
bucket[quotient][j]=input[i];
break;
}
}
}
//小桶排序
for (int i=0; i<bucket.length; i++){
//insertion sort
for (int j=1; j<bucket[i].length; ++j){
if(bucket[i][j]==null){
break;
}
int value = bucket[i][j];
int position=j;
while (position>0 && bucket[i][position-1]>value){
bucket[i][position] = bucket[i][position-1];
position--;
}
bucket[i][position] = value;
}
}
//輸出
for (int i=0, index=0; i<bucket.length; i++){
for (int j=0; j<bucket[i].length; j++){
if (bucket[i][j]!=null){
input[index] = bucket[i][j];
index++;
}
else{
break;
}
}
}
return input;
}
複製代碼
最佳狀況:T(n) = O(n+k)
最差狀況:T(n) = O(n+k)
平均狀況:T(n) = O(n2)
桶排序的穩定性取決於每一個桶使用的排序算法,像上面的例子中使用了插入排序.就是穩定的,若是使用了快排,那就是不穩定的.
其實我我的感受,桶排序更像是一種思路,而不是像快速排序
,插入排序
等是一種具體的算法
.
桶排序,思路就是將待排序數組,按照必定的映射規則分桶,好比,f(x)=x/10
,那麼就是按十位分組,12,13
在一個桶,25,23
在一個桶.而後對每一個桶使用其餘排序算法進行排序,固然你也能夠對每一個桶繼續使用桶排序.
桶排序有一些限制,即數據必須比較均勻.假設待排序數組爲1,2,3,50000
.依然按照十位分桶,那麼會生成5000個桶,其中只有一個桶裏有3個數字,一個桶裏有一個數字,其他徹底爲空.不只浪費了大量的內存,也沒有起到提升效率的做用.
基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此是穩定的。
public int[] sort(int[] input) {
if (input == null || input.length < 2)
return input;
// 1.先算出最大數的位數;
int max = input[0];
for (int i = 1; i < input.length; i++) {
max = Math.max(max, input[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
//二維數組,第一維是桶,第二維是桶裏的元素
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
//對mod取模,除以div,能夠得到數字在當前位的數字,放進合適的桶裏.
for (int j = 0; j < input.length; j++) {
int num = (input[j] % mod) / div;
bucketList.get(num).add(input[j]);
}
int index = 0;
//將每一個通內的元素按順序拿出來,此時的順序已是按照當前位排序後的元素
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
input[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return input;
}
複製代碼
最佳狀況:T(n) = O(n * k)
最差狀況:T(n) = O(n * k)
平均狀況:T(n) = O(n * k)
穩定,分桶和從桶裏取出均可以保證穩定性.
基數排序也是經過分桶的思路,不過在上面例子中,桶的數量固定爲10.由於每一位上的數字只有10種可能.
利用桶,每次排序一個位,當最高位也排序以後,序列變爲有序序列.
看代碼能夠發現,這三種排序都用到了桶.
基數排序: 桶固定爲10個,用來放置當前位等於桶下標的數字.
計數排序: 每一個桶只有一系列相同的數字,桶的數量爲最大元素減去最小元素的數量.
桶排序: 每一個桶放置必定範圍內的數字,具體範圍能夠自定義.
百度百科
以上皆爲我的所思所得,若有錯誤歡迎評論區指正。
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------><a href=http://huyan.couplecoders.tech">呼延十