排序算法是最基本的知識了,面試中也是常考的知識點,尤其是快速排序考察最多,也要求手寫出來,並分析他的時間複雜度。下面是我對所學到的知識的一個總結。
排序算法總共分爲5大類:
第一類:插入排序 直接插入、希爾排序
第二類:選擇排序簡單選擇排序 堆排序
第三類:交換排序冒泡排序 快速排序
第四類:歸併排序
第五類:基數排序(考察很少,知道概念就行)
直接插入排序
方法:假設元素從小到大排序,先固定第一個元素,讓第二個元素開始往後查找,把後面的元素放到一個臨時變量裏面拎出來,讓他和他前面的元素比較,如果比前面的元素小,就把被比較的元素向後面移動一格,並且指針向前移動一位,確保臨時變量是依次和前面的元素比較的。此方法需要兩個指針。填充位置爲每次比較排好序的元素。
需要注意確定邊界值,j指針>=0
時間複雜度計算:(1+n)*n/2=O(n^2)相當於九九乘法表的圖形
#include <iostream>
using namespace std;
void StraightInsertion(int a[], int length)
{
int i = 1, j = 0;
int temp = 0;
/*當前排到第幾個元素*/
for (i;i<length;i++)
{
/*保存當前的元素*/
temp = a[i];
/*依次檢索是否小於前面的元素*/
j = i - 1;
while (temp < a[j] && j >= 0)
{
/*當前被檢測元素向後移動一格*/
a[j + 1] = a[j];
j--;
}
/*找到插入位置*/
a[j + 1] = temp;
}
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
StraightInsertion(a, 10);
return 0;
}
希爾排序
方法:把整個一串數按照/2進行分組,10個數/2先分成5組,然後5/2=2,再分成兩組,直到爲1,這樣再進行組內直接插入排序。只是單純的比直接插入排序多了一個分組的步驟。
代碼和直接插入幾乎一樣,只不過把j移動的距離從1改爲組的個數h
平均時間複雜度提高了,可以做到部分有序,再進行直接插入
這裏注意:組間是交叉進行組內比較的,不是一個組進行完進行下一個組。
該算法是不穩定的。
#include <iostream>
using namespace std;
void shellSort(int a[], int length)
{
int i = 0, j = 0, temp = 0, h = length/2;/*組的個數*/
//分組
for (h;h>=1;h/=2)
{
/*各組交替比較組內的元素 也就是使用直接插入排序*/
for (i = h; i < length; i++)
{
temp = a[i];
j = i - h;
while (temp < a[j] && j >= 0)
{
/*當前被檢測元素在組內向後移動一個元素*/
a[j + h] = a[j];
j -= h;
}
/*找到插入位置*/
a[j + h] = temp;
}
}
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
shellSort(a, 10);
return 0;
}
簡單選擇排序
思想:從一堆無序的數字中,先選擇一個最小的,然後選擇次小的,一次類推。。是最簡單的排序,符合人們的正常思維。
方法:需要額外開闢一個數組,用來存儲拿出來的元素。首先讓一串數字的首元素默認是當前最小值,記錄他的標號,用min_index表示,然後讓後面的元素依次和標號所只的元素比較,若比他小,記錄下標號,先不着急交換,因爲沒有比較完,後面可能有更小的,直到比較到頭,然後此時
min_index是最小的值的標號,然後將第一個元素和其交換,此時最小的找到,接着找次小的,此時i指針後移一位,j接着從i的下一個開始往後捋。(i的範圍是倒數第二個數字,不用再比較最後歐一個元素,因爲前面都是小的了,自然最後的是最大的值)。
該算法不穩定,對未排序的算法進行了未知的修改。
#include <iostream>
using namespace std;
void exchange(int a[], int i, int j)
{
int temp = 0;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
void selectionSort(int a[], int n)
{
int i, j, min_index;
for ( i = 0; i < n - 1; i++)
{
min_index = i;
for (j=i+1;j<n;j++)
{
if (a[j] < a[min_index])
/*找到本次最小值*/
min_index = j;
}
if (min_index != i)
/*交換*/
exchange(a, min_index, i);
}
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
selectionSort(a, 10);
return 0;
}
堆排序
需要用到樹的結構,需要的是一個完全二叉樹。
步驟:
1。建立一個大根/小根堆
2.堆化
3.直接選擇排序
若從小到大排序,先建立一個大根堆,然後按層遍歷到一個數組,然後將收尾元素交換,此時保證了最後的元素是最大的值,之後讓iCurront指針向前移動一位,將前面的元素進行堆化,用到遞歸。遞歸的終止條件是該元素是葉子結點,沒有左孩子了,也就是說該元素的左孩子已經是排好序的無效結點了。如何找到那個有效結點呢?公式:(n-1)/2 n代表最後的元素下標
該算法不穩定。
#include <iostream>
using namespace std;
int iCurrentPos = 0;
void exchange(int a[], int b, int c)
{
int temp = 0;
temp = a[b];
a[b] = a[c];
a[c] = temp;
}
void heapify(int a[], int indexRoot)
{
int indexL = indexRoot * 2 + 1, indexR = indexL + 1;
int indexMax = indexRoot;
/*終止條件,當時葉子節點的時候 如何判斷是否是葉子節點,
就是看他的左孩子是否是無效的節點
*/
if (indexL > iCurrentPos)
return;
/*比較左子節點*/
if (a[indexL] > a[indexMax])
indexMax = indexL;
/*比較右子節點*/
if (a[indexR] > a[indexMax] && indexR < iCurrentPos)
indexMax = indexR;
/*交換結點*/
if (indexMax != indexRoot)
{
exchange(a, indexRoot, indexMax);
heapify(a, indexMax);
}
}
/*建立一個大根堆*/
void createHeap(int a[], int n) //n爲第n個元素
{
/*找到有效的結點*/
int i = (n - 1) / 2;
for (i; i >= 0; --i)
{
heapify(a, i);
}
}
/*堆排序*/
void heapSort(int a[], int n) /*n指的總共的元素個數*/
{
iCurrentPos = n - 1;
createHeap(a, n - 1);
for (; iCurrentPos > 0; --iCurrentPos, heapify(a, 0))
exchange(a, 0, iCurrentPos);
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
heapSort(a, 10);
return 0;
}
冒泡排序
冒泡排序就是兩兩交換,然後每一次遍歷都會讓最大放在數組的最後。然後接着從頭倆倆比較。
#include <iostream>
using namespace std;
void exchange(int a[], int i, int j)
{
int temp = 0;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
void SimpleExchange(int a[], int length)
{
int i = 0,/*已排序數量*/ j = 1;
for (i = 0; i < length - 1; i++)
{
for (j = 1; j < length - i; j++)
{
/*檢查是否交換*/
if (a[j] < a[j - 1])
exchange(a,j - 1, j);
}
}
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
SimpleExchange(a, 10);
return 0;
}
快速排序
典型的遞歸的算法。目的是找到一個軸pivot,讓左邊的都比他小,右邊的都比他大。
如何找到這個值的位置呢?
讓頭尾各指一個指針,把首元素的值拿出來,讓他作爲軸,位置空缺,讓尾元素和他比較,是大的就留在位置不動,然後移動j指針向前,直到找到一個元素的值比第一個小,就讓其放在空缺的位置,此時他的位置空缺,再讓i指針向後移動,和拿出的那個首元素接着比較,若小,就不動,指針後移,直到找到比他大的元素,放在空白位置。然後尾指針往前移動。。。直到 i j指針重合就是首元素該插入的位置,就是中間元素,保證了左側的都是小的,右側都是大的.然後左右遞歸。
快排的時間複雜度分析
最佳時間複雜度是nlogn 每次分割成兩半,分多少層呢?就是N不斷的/2,也就是logn層,每層的從左到右交換都是n的操作,所以時間複雜度最佳是nlogn
最壞時間複雜度n^2 在最壞的情況下,待排序的序列爲正序或者逆序,每次劃分只得到一個比上一次劃分少一個記錄的子序列。也就是說每次都是1個數和其餘要進行遞歸的序列。這樣就需要n層,每層是n的操作 即複雜度是n^2
平均複雜度就是第一次劃分是最佳的劃分,但是左右側就是有最壞的情況,平均下來還是nlogn
空間複雜度就是每分一層,都會有一些臨時變量存在,這樣就是看有多少層,就是nlongn
快排的性能和什麼有關係?
和pivot選取的方法有關係。pivot是可以隨便選擇的,但是要注意選到的值是中間的值,還是過大或者過小的。要是極值可能就會得到最壞的時間複雜度。
那如何避免最壞時間複雜度呢?如何優化快排呢?
1.常用的方法就是三數取中。就是找到頭,中間和尾的元素,哪個是中間值,就把哪個作爲軸。
2.直接插入
由於是遞歸程序,每一次遞歸都要開闢棧幀,當遞歸到序列裏的值不是很多時,我們可以採用直接插入排序來完成,從而避免這些棧幀的消耗。
還有一種方法,用的不是很多
讓第一個元素的左邊放一個可以擴展的區域,假設最後的元素是軸,從第一個元素開始比較,如果比軸小,就把他包括進這個區域,如果大了,就把軸前面的元素和這個元素做交換,此時檢查交換後的這個元素是不是還是比軸大,若還是就把他接着和軸前一個的前一個交換。。直到和後面交換的元素重合,然後軸剩下了,放在什麼位置呢?就把他和區的下一個位置做交換。
具體還有好幾種方法,就不一一列舉了。這篇博客說的很明白。
詳見可見博客地址
如今的快排和直接插入聯合使用了。快排當到一定的深度,量少,個數少趨於有序的時候就不佔有優勢,適合直接插入,所以將兩者聯合使用效果更佳。微軟目前使用的就是這樣的排序算法。
#include <iostream>
using namespace std;
void QuickSort(int a[], int min,int max)
{
/*遞歸的結束條件*/
if (min >= max)
return;
int pivot = a[min];
int _min = min, _max = max;
while (min < max)
{
/*從大端開始比較*/
while (a[max] >= pivot && min<max)
max--;
/*如果不再大於等於pivot 就進行交換*/
if (min < max)
{
a[min] = a[max];
min++;
}
/*從小端開始比較*/
while (a[min] <= pivot && min < max)
min++;
/*如果不再小於等於pivot,就交換*/
if (min < max)
{
a[max] = a[min];
max--;
}
}
/*找到中樞的位置pivot*/
a[min] = pivot;
/*遞歸左側*/
QuickSort(a, _min, min - 1);
/*遞歸右側*/
QuickSort(a, max + 1, _max);
}
int main(void)
{
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
QuickSort(a,0,9);
return 0;
}
歸併排序
視頻鏈接
歸併算法和前幾種都不一樣,是一個特殊的算法,他就是兩個步驟,先分解,再整合。
Divide and Conquer(分而治之)
實現思路:我們需要開闢一個數組,然後將分解出的數組在頭尾均設立指針,左側最小,左側最大,右側最小,右側最大。數組需要遍歷的元素就是左側最小到右側最大。
歸併時分三種情況討論:
1.當左右兩側均沒有到末尾,就比較左右的誰最小,放到數組的相應位置。然後移動指針和數組的指針。
2.當左側到了頭,就說明右側有小的,就把右側的元素給數組並移動指針。
3.當右側到了頭,說明左側有小的。把左側的元素給數組並移動指針。
#include <iostream>
using namespace std;
void merge(int a[], int min_l, int max_l, int min_r, int max_r,int temp[])
{
int k, i = min_l, j = min_r;
for (k=min_l;k<=max_r;k++)
{
/*左右兩側均沒到結尾*/
if (i <= max_l && j <= max_r)
{
if (a[i] <= a[j])
temp[k] = a[i++];
else
temp[k] = a[j++];
}
/*左側到了結尾*/
else if (i>max_l)
temp[k] = a[j++];
/*右側到結尾*/
else
temp[k] = a[i++];
}
/*把值付給數組a 數據填回*/
for (k=min_l;k<=max_r;k++)
{
a[k] = temp[k];
}
}
void MergingSort(int a[],int min,int max,int temp[])
{
/*終止條件*/
if (min>=max)
return;
/*divide*/
int middle = min+(max - min) / 2;
MergingSort(a, min, middle,temp);
MergingSort(a, middle + 1, max,temp);
/*conquer*/
merge(a, min, middle, middle + 1, max,temp);
}
int main(void)
{
int b[10] = {0};
int a[10] = { 1,5,3,4,7,8,9,2,10,6 };
MergingSort(a, 0, 9, b);
return 0;
}