一、冒泡排序
思路:冒泡排序算法需要遍歷幾次數組。每次遍歷都要比較連續相鄰的元素,如果某一對相鄰元素是降序,則互換它們的值,否則,保持不變。由于較小的值像“氣泡”一樣逐漸浮想頂部,而較大的值沉向底部,所以叫冒泡排序。平均時間復雜度為O(n^2)。
最差的情況下,冒泡排序算法需要進行n-1次遍歷。第一次遍歷需要n-1次比較,第二次遍歷需要n-2次比較,依次進行;因此比較總數為:
(n-1)+(n-2)+...+2+1=n(n-1)/2=O(n2)
最差的情況冒泡排序的時間復雜度為O(n^2)。
冒泡算法的改進:
冒泡排序的效率比較低,所以我們要通過各種方法改進。在上例中,第四輪排序之后實際上整個數組已經是有序的了,最后兩輪的比較沒必要進行。
注意:如果某次遍歷中沒有發生交換,那么就不必進行下一次遍歷,因為所有元素已經排好了。
所以最好的情況是數據本來就有序,復雜度為O(n)。
平均時間復雜度為O(n^2), 最差的情況冒泡排序的時間復雜度為O(n^2),最好的情況是數據本來就有序,復雜度為O(n)。
算法是穩定的,因為當a=b時,由于只有大于才做交換,故a和b的位置沒有機會交換,所以算法穩定。
空間復雜度為O(1),不需要額外空間。
二、選擇排序
思路:選擇排序改進了冒泡排序,將必要的交換次數從O(n2)減少到O(n),但是比較次數仍保持為O(n2)。冒泡排序每比較一次就可能交換一次,但是選擇排序是將一輪比較完后,把最小的放到最前的位置(或者把最大的放到最后)。
算法分析:選擇排序最好和最壞的情況一樣運行了O(N2)時間,平均復雜度也是O(N2)。
算法是不穩定的,假設a=b,且a在b前面,而某輪循環中最小值在b后面,而次最小值需要跟a交換,這時候b就在a前面了,所以選擇排序是不穩定的。
空間復雜度為O(1),不需要額外的空間。
三、插入排序
四、快速排序
思路:雖然快速排序稱為分治法,但分治法這三個字顯然無法很好的概括快速排序的全部步驟。比較完整的來說應該是:挖坑填數+分治法:
挖坑填數的說明:L:最左邊索引,R:最右邊索引
1.i =L; j = R; 將基準數即最左邊第一個數挖出形成第一個坑a[i]。
while循環{
2.j--由后向前找比基準數小的數,找到后挖出此數填前一個坑a[i]中。
3.i++由前向后找比基準數大的數,找到后也挖出此數填到前一個坑a[j]中。
}
4.再重復執行2,3二步,直到i==j,將基準數填入a[i]中。
5.這樣整個數據中就被基準數分為了兩個區間,左邊比它小,右邊比它大。
分治法說明:再通過遞歸對左右區間重復第二步,直到各區間只有一個數。
完整的代碼如下:
//快速排序:挖坑填數+分治法
void fastSort(int *arr, int left, int right)
{
if (left<right && arr!=NULL) {
int i = left;//left、right要保留,最后要用
int j = right;
int key = arr[left];
/******挖坑填數******/
//每個大while循環:對left作為基準值進行了分區,小的放在了左邊,大的放在了右邊
while (i<j) {
while (i<j && arr[j]>=key) {
j--;
}
arr[i]=arr[j];//拿j(后邊)的數填到i(前邊)的坑里
while (i<j && arr[i]<=key) {
i++;
}
arr[j]=arr[i];//拿i(前邊)的數填到j(后邊)的坑里
}
arr[i]=key;
/******挖坑填數******/
/******分治法******/
fastSort(arr, left, i-1);
fastSort(arr, i+1, right);
/******分治法******/
}
}
1、n大時好,快速排序比較占用內存,內存隨n的增大而增大,但卻是效率高不穩定的排序算法。
2、最差的情況是本身是有序數組,劃分之后一邊是一個,一邊是n-1個,這種極端情況的時間復雜度就是O(N^2)。最壞的情況下退化成插入排序了。
3、理想的情況是,每次劃分所選擇的中間數恰好將當前序列幾乎等分,經過log2N趟劃分,便可得到長度為1的子表。這樣,整個算法的時間復雜度為O(Nlog2N)。
4、快速排序的平均時間復雜度也是O(Nlog2N)。因此,該排序方法被認為是目前最好的一種內部排序方法。
五、歸并排序
思路:將待排序序列R[0...n-1]看成是n個長度為1的有序序列,將相鄰的有序表成對歸并,得到n/2個長度為2的有序表;將這些有序序列再次歸并,得到n/4個長度為4的有序序列;如此反復進行下去,最后得到一個長度為n的有序序列。
那么歸并是如何進行的呢?
我們稱 R[low, mid] 第一段,R[mid+1, high] 為第二段。每次從兩個段中取出一個記錄進行關鍵字的比較,將較小者放入R2中。最后將各段中余下的部分直接復制到R2中。經過這樣的過程,R2已經是一個有序的序列,再將其復制回R中,一次合并排序就完成了。
迭代:
//輔助函數:合并兩個分組
//mid是第一個分組的末尾的index
void merge(int *arr, int *tmp, int start, int mid, int end)
{
int i = start;
int j = mid+1;
int tmpIndex = start;
//注意:兩個分組的意思,其實自始至終都是在原數組中,只是通過index我們把它看成了兩個分組
while (i<=mid && j<=end) {//兩個分組從第一個開始對比,誰小誰先放進新數組中,直到有一個分組沒有數據了
if (arr[i]<=arr[j]) {
tmp[tmpIndex] = arr[i];
tmpIndex++;
i++;
}
else
{
tmp[tmpIndex] = arr[j];
tmpIndex++;
j++;
}
}
//前一個分組沒放完,把剩余的直接都放到新數組后面即可
while (i<=mid) {
tmp[tmpIndex] = arr[i];
tmpIndex++;
i++;
}
//后一個分組沒放完,把剩余的直接都放到新數組后面即可
while (j<=end) {
tmp[tmpIndex] = arr[j];
tmpIndex++;
j++;
}
//把新數組里的排好序的放回到原數組中
while (start<=end) {
arr[start] = tmp[start];
start++;
}
}
//歸并排序入口
void mergeSort(int *arr,int length)
{
int tmp[length];
int gap = 1;//每組有幾個
while (gap < length) {
int start = 0;
for (start = 0; start+2*gap-1 < length; start=start+2*gap) {
merge(arr, tmp, start, start+gap-1, start+2*gap-1);
}
//此時,說明后邊還有數據(情況1:兩個分組,后邊那個分組,不滿gap;情況2:只有一個分組)
if (start+gap-1<length) {
merge(arr, tmp, start, start+gap-1, length-1);
}
gap = 2*gap;
}
}
遞歸:
void merge(int arr[], int start, int mid, int end) {
int result[ArrLen];
int k = 0;
int i = start;
int j = mid + 1;
while (i <= mid && j <= end) {
if (arr[i] < arr[j]){
result[k++] = arr[i++];
}
else{
result[k++] = arr[j++];
}
}
if (i == mid + 1) {
while(j <= end)
result[k++] = arr[j++];
}
if (j == end + 1) {
while (i <= mid)
result[k++] = arr[i++];
}
for (j = 0, i = start ; j < k; i++, j++) {
arr[i] = result[j];
}
}
void mergeSort(int arr[], int start, int end) {
if (start >= end)
return;
int mid = ( start + end ) / 2;
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
merge(arr, start, mid, end);
}
int main()
{
int arr[] = {4, 7, 6, 5, 2, 1, 8, 2, 9, 1};
mergeSort(arr, 0, 9);
return 0;
}
歸并排序的形式就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的可以得出它的時間復雜度是O(n*log2N)。
歸并排序是穩定的。
六、堆排序
若從空間復雜度來考慮:首選堆排序,其次是快速排序,最后是歸并排序。
若從穩定性來考慮,應選取歸并排序,因為堆排序和快速排序都是不穩定的。
若從平均情況下的排序速度考慮,應該選擇快速排序。