我是自動化專業的應屆研究生,最終拿到了tplink、華爲、vivo等公司的ssp的offer,分享自己學習過的計算機基礎知識(C語言+操作系統+計算機網絡+linux)以及數據結構與算法的相關知識,保證看完讓你有所成長。
歡迎關注我,學習資料免費分享給你哦!還有其他超多學習資源,都是我自己學習過的,經過過濾之後的資源,免去你還在因爲擁有大量資源不知如何入手的糾結,讓你體系化學習。
內部排序
排序算法因爲數據量的不同,就是一次是否可以全部讀入內存分爲外部排序和內部排序。內部排序就是數據可以一次全部讀入內存的情況。
插入排序
插入排序就是從頭至尾逐漸有序的過程。對於一個數組A,從下標爲i=1開始,首先將下標爲1的元素取出A[1],暫時存儲起來tmp。然後逐漸與i前面的元素比較,只要元素比tmp大,那麼就需要將這個元素後移,這也是爲什麼需要暫時把A[1]存儲起來,因爲防止把前面元素後移的時候把這個值覆蓋。這樣直到比較到比tmp小的,此時說明tmp應該插入到這個位置上。這就是插入排序的基本思路。來看一下具體的過程。
首先i等於1,將A[1]=15存入tmp,然後比較A[0]與tmp,發現A[0]大於tmp,所以將A[0]後移到A[1],然後A[0]已經到了數組的界限,不能在比較了,所以tmp插入到A[0]處。然後此時i加1,等於2,將A[2]的元素取出放在tmp中。
重複之前的步驟,比較A[1]和tmp,發現40大於21,那麼將40後移。
在比較A[0]和tmp,發現A[0]小於tmp,那麼此時說明A[0]前面的所有元素都是比tmp小的(如果A[0]前面還有元素的話,因爲我們是從首開始進行的插入排序,說明A[i]以前的元素都是排序好的,如果有一個元素比A[i]小的話,那麼它前面的所有元素都是比tmp小的。)此時將tmp插入到A[1].然後在將i+1,此時i等於3,將A[3]放入tmp。
此時比較A[2]與tmp,發現A[2]比tmp小,說明tmp還是插入在A[3]處,還是前面所說A[2]前面都是排好序的,如果A[2]比tmp小,那麼A[2]之前的所有元素都比tmp小。這就是插入排序的基本過程。下面來看實現代碼。
//A是數據的數組,n是數組中元素的個數
void insert_sort(int *A,int n)
{
int i,j,tmp;
for(i=1;i<n;i++)
{
tmp=A[i];
for(j=i;j>0&&A[j-1]>tmp;j--)
{
A[j]=A[j-1];
}
A[j]=tmp;
}
}
冒泡排序
冒泡排序的思路與插入排序不同,它是將已經排好序的數據放在最後,逐漸縮小需要排序的範圍,最開始是整個數組A的大小n,從數值的開始比較j=0,如果A[j]>A[j+1],那麼就交換兩個元素,這樣就保證了A[j+1]是兩者之間最大的,然後j=j+1,然後繼續比較A[j]與A[j+1],如果A[j]>A[j+1],那麼繼續交換兩個元素。直到比較到j+1==(n-1)爲止,因爲每一次都是相當於把j之前的元素最大值與A[j+1]比較,所以這一輪下來之後,就是把數組中的最大值放在了數組的最後一個位置。然後下一輪的排序就會比較到n-2停止,就是把數組中第二大的元素放在數組的倒數第二個位置了,如此反覆,實現了排序。
首先比較21和15,21大於15,所以交換,然後將j加1.
然後比較21和40,21小於40,不交換。將j加一。
比較40與100,40小於100,不交換。將j加一。
比較100與24,100大於24,交換兩個元素。j加一。可以看到j所指向的元素,一定是j掃描過的最大元素,但j掃描過的不一定有序,下標0-3都是掃描過的,但是並不滿足遞增的順序。
最後比較100與31,100大於31,將100和31交換。完成了一輪排序。此時最後一個元素就是數組中最大的元素,然後將比較界限前移一個位置,j從0重新開始掃描,直到比較界限達到1,這就完成了排序。
懂了基本思想,就可以將其轉換爲代碼了,如下:
void swap(int *a,int *b)
{
int tmp;
tmp=*a;
*a=*b;
*b=tmp;
}
void bubble_sort(int *A,int n)
{
int i,j;
for(i=n-1;i>0;i--)
{
for(j=0;j<i;j++)
{
if(A[j]>A[j+1])
{
swap(&A[j],&A[j+1]);
}
}
}
}
假如在某一輪循環之後,數組已經是排好序的了,也就是沒有必要在繼續去循環了,那麼應該採用什麼辦法呢?其實很簡單,就是判斷是否發生了交換,如果一輪循環判斷下來,沒有發生交換,說明已經排好序了,所以這個時候停止就可以了。
void swap(int *a,int *b)
{
int tmp;
tmp=*a;
*a=*b;
*b=tmp;
}
void bubble_sort(int *A,int n)
{
int i,j;
int flag=0;
for(i=n-1;i>0;i--)
{
flag=0;
for(j=0;j<i;j++)
{
if(A[j]>A[j+1])
{
swap(&A[j],&A[j+1]);
flag=1;
}
}
if(flag==0)
{
break;
}
}
}
希爾排序
希爾排序其實就是改進版本的插入排序,它不是直接對於所有數據做插入排序,而是採用了將數據分組,一個組一個組的做插入排序,然後將分組擴大,在插入排序,直到分組就是整個數組,完成排序。
假設有如上的待排序數組,首先選取分組的間隔,將數組分組,首先選取incre=n/2=6/2=3,那麼就有如下分組
然後每個分組分別插入排序,可以得到如下的順序:21,24;15,100;31,40;
然後縮小incre=incre/2=1,這樣就是整個數組進行插入排序,就可得到排序完成的數組了。
寫成代碼如下:
void shell_sort(int *A,int n)
{
int i,j,ins,tmp;
for(ins=n/2;ins>0;ins=ins/2)
{
for(i=ins;i<n;i++)
{
tmp=A[i];
for(j=i;j>=ins&&A[j-ins]>tmp;j-=ins)
{
A[j]=A[j-ins];
}
A[j]=tmp;
}
}
}
堆排序
堆排序的思路其實很簡單,在最大堆中,最大的元素在數組首地址處,那麼只需要將最大值與數組最後一個值交換,然後將堆的大小縮小1,原來的範圍爲0-n-1,現在n-1下標處已經存放了最大值的元素,然後將堆的範圍縮小至0-n-2,就是相當於堆的刪除操作,然後重新將0-n-2的堆重新構造成最大堆,本質就是刪除操作,只是將刪除後的元素放在了數組的後面。
//這裏與堆不同的地方在於沒有哨兵了,父節點下標爲i,那子節點下標爲2*i+1、2*i+2
void perdown(int *A,int d,int n)
{
int parent,child;
int data;
data=A[d];
for(parent=d;parent*2+1<n;parent=child)
{
child=parent*2+1;
if((child+1)<n&&(A[child]<A[child+1]))
{
child++;
}
if(data>=A[child])
{
break;
}
A[parent]=A[child];
}
A[parent]=data;
}
void heap_sort(int *A,int n)
{
int i;
for(i=(n-1)/2;i>=0;i--) //將數組構建爲最大堆
{
perdown(A,i,n);
}
for(i=n-1;i>0;i--)
{
swap(&A[0],&A[i]); //將最大交換到堆的最後一個位置
perdown(A,0,i);//重新構建最大堆
}
}
歸併排序
歸併排序是採用了遞歸的思想來逐漸將數組分成兩個部分,比如一個數組有6個元素,首先分成兩個部分,前3個爲一組,後3個爲一組。
然後繼續分組。
將每個元素分到最小之後,然後每兩個進行合併,合併的時候按照順序合併,如下圖15和21合併時交換了位置。
然後繼續合併,直達整個數組排好順序。這就是歸併的思路,首先將排序的數組分離,分離之後在逐漸按照順序合併。
void merge(int *A,int *tmp,int left,int right,int right_end)
{
int left_end,tmpPos=left,lengthofdata;
left_end=right-1;
lengthofdata=right_end-left+1;
while(left<=left_end&&right<=right_end)
{
if(A[left]<A[right])
{
tmp[tmpPos++]=A[left++];
}
else
{
tmp[tmpPos++]=A[right++];
}
}
while(left<=left_end)
{
tmp[tmpPos++]=A[left++];
}
while(right<=right_end)
{
tmp[tmpPos++]=A[right++];
}
for(lengthofdata;lengthofdata>0;lengthofdata--,right_end--)
{
A[right_end]=tmp[right_end];
}
}
void mergesort(int *A,int *tmp,int left,int right_end)
{
int right;
if(left>=right_end)
{
return;
}
right=(left+right_end)/2;
mergesort(A,tmp,left,right);
mergesort(A,tmp,right+1,right_end);
merge(A,tmp,left,right+1,right_end);
}
void merge_sort(int *A,int n)
{
int *tmp;
tmp=(int *)malloc(sizeof(int)*n);
if(tmp!=NULL)
{
mergesort(A,tmp,0,n-1);
free(tmp);
}
else
{
printf("內存不足");
}
}
快速排序
快速排序基本思想與歸併排序類似,歸併排序一個是按照等分的策略,將待排序的數組分成兩部分,然後迭代處理;另一點是需要額外的空間。快速排序不同的地方是不是等分的策略而是選取一個值作爲參考值,將這次排序區域中的比它小的放到左邊,比它大的放在右面,然後將左面和右面分別按照這種辦法進行遞歸求解,直到待排序的值只有一個爲止。
以上面數組爲例,首先選取一個數字作爲分界的參考值,這裏以數組的第一個元素作爲參考值,x=A[0],之後從數組的最前面和最後面開始掃描數組,過程是這樣的,定義i=0,j=9,
從A[j]開始比較,如果A[j]>=x,那麼j就前移,即j–,如果A[j]<x,那麼就將這個值插入到A[i]中,因爲A[i]的值已經保存到了X中,然後i++,在從A[i]開始比較。如果A[i]<x,就i向前移動,即i++。
下圖所示,直到i移動到83這個位置,此時A[i]>=x。
此時將83插入到A[j]的位置,然後j前移,在從A[j]處重複上述過程,直到i>=j結束。
這樣就完成了一次快速排序,紅色部分左側比72小,右側比72大,然後將左側下標爲0-4的數據進行上述過程,右側下標爲7-9的數據進行上述過程,直到只有一個數據。就完成了快速排序,代碼如下:
void q_sort(int *A,int left,int right_end)
{
int privot,i,j;
privot=A[left];
i=left;
j=right_end;
if(i<j)
{
while(1)
{
while(i<j&&A[j]>=privot)
{
j--;
}
if(i<j)
{
A[i++]=A[j];
}
while(i<j&&A[i]<privot)
{
i++;
}
if(i<j)
{
A[j--]=A[i];
}
else
{
break;
}
}
A[i]=privot;
q_sort(A,left,i-1);
q_sort(A,i+1,right_end);
}
}
//爲了使排序接口一致
void quick_sort(int *A,int n)
{
q_sort(A,0,n-1);
}
排序算法時間複雜度總結
排序方法 | 平均時間複雜度 | 最壞時間複雜度 | 額外空間複雜度 | 穩定性 |
---|---|---|---|---|
插入排序 | O(n^2) | O(n^2) | O(1) | 穩定 |
冒泡排序 | O(n^2) | O(n^2) | O(1) | 穩定 |
歸併排序 | O(nlogn) | O(nlogn) | O(n) | 穩定 |
希爾排序 | O(n^d) | O(n^2) | O(1) | 不穩定 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不穩定 |
快速排序 | O(nlogn) | O(n^2) | O(logn) | 不穩定 |