1. 冒泡排序 Time Complexity:O(N2) Space Complexity:O(1)
关键点:
a. 外层循环i从0循环到N-2共N-1次(N-1个数排好了,N个数也就排好了);
b. 内层循环j每次从1到N-i-1,范围逐渐变小(后面排好的数逐渐变多);
c. 每次比较和交换都是相邻的数
void BubbleSort(int A[], int N)
{
int tmp;
for (i = 0; i < N - 1; ++i)
{
for (j = 1; j < N - i; ++j)
{
if (A[j] > A[j - 1]) //从大到小排序,把较小的交换到后面来
{
tmp = A[j - 1];
A[j - 1] = A[j];
A[j] = tmp;
}
}
}
}
缺点:交换次数太多
是否稳定:是
其他:比较次数始终为仍为 N - 1 + N –2 + … + 1 = N(N-1)/2;
在数组元素已有序的情况下,不用移动元素;
数组逆序情况下,移动次数 3N(N - 1)/2;
小改进:
void BubbleSort(int A[], int N)
{
int tmp;
bool exchanged;
for (i = 0; i < N - 1; ++i)
{
exchanged = false;
for (j = 1; j < N - i; ++j)
{
if (A[j] > A[j - 1]) //从大到小排序,把较小的交换到后面来
{
exchanged = true;
tmp = A[j - 1];
A[j - 1] = A[j];
A[j] = tmp;
}
}
if(!exchanged)
return;
}
}
可以把数组有序情况下的比较次数减少为N-1,其他改进参考[1]
2. 插入排序 Time Complexity:O(N2) Space Complexity:O(1)
关键点:
a. i从1到N-1循环,假设前i个元素都是排序好(这里以从大到小为例)的
b. 将但前元素保存到tmp值,并从后向前(从小到大)和前面的元素比较,如果该元素值需要往前面移动,则将前面的元素向后挪动。
c. 这是一个比较和移动相互结合的过程,和先找到当前元素应该插入的位置,然后再统一移动元素有点不同。
//从大到小排序
void InsertSort(int A[], int N)
{
int i, j, tmp;
for (i = 1; i < N; ++i)
{
tmp = A[i];
for (j = i - 1; j >= 0; --j)
{
if (tmp > A[j])
{
A[j + 1] = A[j];
}
else
break;
}
A[j + 1] = tmp;
}
}
是否稳定:是
当数组有序时,比较N-1次,移动2(N-1)次;
当数组逆序时,比较N(N-1)/2次,移动(N-1)(N+4)/2次
3. 选择排序 Time Complexity:O(N2) Space Complexity:O(1)
(1)不稳定方法
关键点:
a. 进行N-1次选择,每次选择都从剩下的未排序数组中选出最大的数放到当前轮最前面;
b. 选择过程只是记录最值下标;
c. 当最值下标和当前轮元素下标不同时实施移动;
d. 移动仅仅是交换,那么该方法是不稳定的;
e. 选择算法的比较次数无论如何都不会改变,最好情况下移动次数为0;
//从大到小排序
void SelectSort(int A[], int N)
{
int i, j, k;
int tmp;
for (i = 0; i < N - 1; ++i)
{
k = i;
for (j = i + 1; j < N; ++j)
{
if (A[j] > A[k])
{
k = j;
}
}
if (k != i)
{
tmp = A[k];
A[k] = A[i];
A[i] = tmp;
}
}
}
以上算法最坏情况下移动次数为 3(N-1)
如果移动是将最值插入到该轮元素前面,那么选择算法就是稳定的:
void SelectSortStable(int A[], int N)
{
int i, j, k;
int tmp;
for (i = 0; i < N - 1; ++i)
{
k = i;
for (j = i + 1; j < N; ++j)
{
if (A[j] > A[k])
{
k = j;
}
}
if (k != i)
{
tmp = A[k];
for (j = k; j > i; --j)
{
A[j] = A[j - 1];
}
A[i] = tmp;
}
}
}
上述算法最坏情况下移动次数为 N+1 + N + N-1 + … + 3 = (N+4)(N-1)/2
4. 归并排序 Time Complexity:O(Nlog2N) Space Complexity:O(N)
关键点:
a. 使用递归过程,直到子数组只有一个元素时停止;
b. 使用N个空间的辅助数组;
c. 递归时有栈开销;
d. 归并排序是O(Nlog2N) 复杂度的稳定排序
//从大到小排序
void Merge(int *arr, int start, int mid, int end, int *tmparr)
{
int i, j, k = 0;
for (i = start, j = mid + 1; i <= mid && j <= end;)
{
if (arr[i] >= arr[j])
tmparr[k++] = arr[i++];
else
tmparr[k++] = arr[j++];
}
for (;i <= mid;)
{
tmparr[k++] = arr[i++];
}
for (;j <= end;)
{
tmparr[k++] = arr[j++];
}
//将排序后的辅助数组拷贝到原始数组
for (i = 0; i < (end - start + 1); ++i)
{
arr[start+i] = tmparr[i];
}
}
void MergeSort(int * arr, int start, int end, int *tmparr)
{
if (start < end)
{
int mid = (start + end)/2;
MergeSort(arr, start, mid, tmparr);
MergeSort(arr, mid + 1, end, tmparr);
Merge(arr, start, mid, end, tmparr);
}
}
调用方法:MergeSort(arr, 0, N - 1, tmparr);
非递归的算法如下,SubListSize为合并单元的长度,依次从1,2,4,…2n 增长,直到超过数组的总长度位置。对于从小到大的每个SubListSize,对原数组依次分组处理,每组的长度都为2*SubListSize(末尾不足的除外),这样从短到长一步步合并,最后得到排序后的数组。其优点是没有函数调用栈的开销。
void Mergesort( ElementType A[ ], int N )
{
ElementType *TmpArray;
int SubListSize, Part1Start, Part2Start, Part2End;
TmpArray = malloc( sizeof( ElementType ) * N );
for( SubListSize = 1; SubListSize < N; SubListSize *= 2 )
{
Part1Start = 0;
while( Part1Start + SubListSize < N - 1 )
{
Part2Start = Part1Start + SubListSize;
Part2End = min( N, Part2Start + SubListSize - 1 );
Merge( A, TmpArray, Part1Start, Part2Start, Part2End );
Part1Start = Part2End + 1;
}
}
free(TmpArray);
}
5. 堆排序 Time Complexity:O(Nlog2N) Space Complexity:O(1)
堆排序就是将数组中的元素逻辑上组织成完全二叉树的形式,排序算法核心在于堆的调整。创建堆时,是从后向前构造,即从下往上的根节点构造。调整时,是将对顶元素向下调整。
堆排序时,依次交换堆顶元素和数组末尾元素,然后缩小堆长度并调整堆。如果需要从大到小排序,则建立小顶堆,如果从小到大排序,则建立大顶堆。堆排序是不稳定排序。
下面是从大到小的堆排序算法,设数组有效元素从1开始
void HeapAdjust(int *arr, int r, int m)
{
int rv = arr[r];
int j;
for (j = 2 * r; j <= m; j = 2 * j)
{
if (j < m && arr[j+1] < arr[j])
{
j++;
}
if (arr[j] < rv)
{
arr[r] = arr[j];
r = j;
}
else
break;
}
arr[r] = rv;
}
void HeapSort(int *arr, int N)
{
int i, tmp;
for (i = N/2; i > 0; --i)
{
HeapAdjust(arr, i, N);
}
for (i = N; i > 1; --i)
{
tmp = arr[1];
arr[1] = arr[i];
arr[i] = tmp;
HeapAdjust(arr, 1, i - 1);
}
}
下面是数组元素从0开始的写法:
void HeapAdjust(int *arr, int r, int len)
{
int rv = arr[r];
int j;
for (j = 2 * r + 1; j < len; j = 2 * r + 1)
{
if (j < len - 1 && arr[j+1] < arr[j])
{
j++;
}
if (arr[j] < rv)
{
arr[r] = arr[j];
r = j;
}
else
break;
}
arr[r] = rv;
}
void HeapSort(int *arr, int N)
{
int i, tmp;
for (i = N/2 - 1; i >= 0; --i)
{
HeapAdjust(arr, i, N);
}
for (i = N - 1; i > 0; --i)
{
tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
HeapAdjust(arr, 0, i);
}
}
调用方式都是:HeapSort(arr, N);
6. 快速排序
快速排序是一般情况下使用最多的排序方法,它的平均时间复杂度为O(Nlog2N),虽然在最坏情况下可能达到O(N2),快速排序空间复杂度为O(1),一般使用递归实现,快速排序是一种不稳定的排序算法。
下面是一种经典的实现,从大到小排序:
int Partition(int *arr, int low, int high)
{
int pivot = arr[low];
while(low < high)
{
while(low < high && arr[high] <= pivot) high--;
arr[low] = arr[high];
while(low < high && arr[low] >= pivot) low++;
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
void Qsort(int *arr, int low, int high)
{
if (low < high)
{
int mid = Partition(arr, low, high);
Qsort(arr, low, mid - 1);
Qsort(arr, mid + 1, high);
}
}
一种考虑更全面的实现如下,三数中值分割算法,从小到大排序:
int Median3(int A[], int Left, int Right)
{
int Center = (Left + Right)/2;
if (A[Left] > A[Center])
Swap(&A[Left], &A[Center]);
if (A[Left] > A[Right])
Swap(&A[Left], &A[Right]);
if (A[Center] > A[Right])
Swap(&A[Center], &A[Right]);
//A[Left] <= A[Center] <= A[Right]
Swap(&A[Center], &A[Right - 1]); //Hide pivot
return A[Right - 1]; //Return pivot
}
void Qsort3(int A[], int Left, int Right)
{
int i, j;
int Pivot;
//这里用了一个阈值,当数组长度小于阈值时用简单插入排序,比较高效
if (Left + Cutoff <= Right)
{
Pivot = Median3(A, Left, Right);
i = Left; j = Right - 1;
for ( ; ;)
{
while( A[++i] < Pivot ) {}
while( A[--j] > Pivot ) {}
if (i < j)
Swap(&A[i], &[j]);
else
break;
}
Swap(&A[i], &A[Right - 1]); //Restore pivot
Qsort3(A, Left, i - 1);
Qsort3(A, i + 1, Right);
}
else
InsertSort(A, Left, Right - Left + 1);
}
关键点:
a. 外层循环i从0循环到N-2共N-1次(N-1个数排好了,N个数也就排好了);
b. 内层循环j每次从1到N-i-1,范围逐渐变小(后面排好的数逐渐变多);
c. 每次比较和交换都是相邻的数
void BubbleSort(int A[], int N)
{
int tmp;
for (i = 0; i < N - 1; ++i)
{
for (j = 1; j < N - i; ++j)
{
if (A[j] > A[j - 1]) //从大到小排序,把较小的交换到后面来
{
tmp = A[j - 1];
A[j - 1] = A[j];
A[j] = tmp;
}
}
}
}
缺点:交换次数太多
是否稳定:是
其他:比较次数始终为仍为 N - 1 + N –2 + … + 1 = N(N-1)/2;
在数组元素已有序的情况下,不用移动元素;
数组逆序情况下,移动次数 3N(N - 1)/2;
小改进:
void BubbleSort(int A[], int N)
{
int tmp;
bool exchanged;
for (i = 0; i < N - 1; ++i)
{
exchanged = false;
for (j = 1; j < N - i; ++j)
{
if (A[j] > A[j - 1]) //从大到小排序,把较小的交换到后面来
{
exchanged = true;
tmp = A[j - 1];
A[j - 1] = A[j];
A[j] = tmp;
}
}
if(!exchanged)
return;
}
}
可以把数组有序情况下的比较次数减少为N-1,其他改进参考[1]
2. 插入排序 Time Complexity:O(N2) Space Complexity:O(1)
关键点:
a. i从1到N-1循环,假设前i个元素都是排序好(这里以从大到小为例)的
b. 将但前元素保存到tmp值,并从后向前(从小到大)和前面的元素比较,如果该元素值需要往前面移动,则将前面的元素向后挪动。
c. 这是一个比较和移动相互结合的过程,和先找到当前元素应该插入的位置,然后再统一移动元素有点不同。
//从大到小排序
void InsertSort(int A[], int N)
{
int i, j, tmp;
for (i = 1; i < N; ++i)
{
tmp = A[i];
for (j = i - 1; j >= 0; --j)
{
if (tmp > A[j])
{
A[j + 1] = A[j];
}
else
break;
}
A[j + 1] = tmp;
}
}
是否稳定:是
当数组有序时,比较N-1次,移动2(N-1)次;
当数组逆序时,比较N(N-1)/2次,移动(N-1)(N+4)/2次
3. 选择排序 Time Complexity:O(N2) Space Complexity:O(1)
(1)不稳定方法
关键点:
a. 进行N-1次选择,每次选择都从剩下的未排序数组中选出最大的数放到当前轮最前面;
b. 选择过程只是记录最值下标;
c. 当最值下标和当前轮元素下标不同时实施移动;
d. 移动仅仅是交换,那么该方法是不稳定的;
e. 选择算法的比较次数无论如何都不会改变,最好情况下移动次数为0;
//从大到小排序
void SelectSort(int A[], int N)
{
int i, j, k;
int tmp;
for (i = 0; i < N - 1; ++i)
{
k = i;
for (j = i + 1; j < N; ++j)
{
if (A[j] > A[k])
{
k = j;
}
}
if (k != i)
{
tmp = A[k];
A[k] = A[i];
A[i] = tmp;
}
}
}
以上算法最坏情况下移动次数为 3(N-1)
如果移动是将最值插入到该轮元素前面,那么选择算法就是稳定的:
void SelectSortStable(int A[], int N)
{
int i, j, k;
int tmp;
for (i = 0; i < N - 1; ++i)
{
k = i;
for (j = i + 1; j < N; ++j)
{
if (A[j] > A[k])
{
k = j;
}
}
if (k != i)
{
tmp = A[k];
for (j = k; j > i; --j)
{
A[j] = A[j - 1];
}
A[i] = tmp;
}
}
}
上述算法最坏情况下移动次数为 N+1 + N + N-1 + … + 3 = (N+4)(N-1)/2
4. 归并排序 Time Complexity:O(Nlog2N) Space Complexity:O(N)
关键点:
a. 使用递归过程,直到子数组只有一个元素时停止;
b. 使用N个空间的辅助数组;
c. 递归时有栈开销;
d. 归并排序是O(Nlog2N) 复杂度的稳定排序
//从大到小排序
void Merge(int *arr, int start, int mid, int end, int *tmparr)
{
int i, j, k = 0;
for (i = start, j = mid + 1; i <= mid && j <= end;)
{
if (arr[i] >= arr[j])
tmparr[k++] = arr[i++];
else
tmparr[k++] = arr[j++];
}
for (;i <= mid;)
{
tmparr[k++] = arr[i++];
}
for (;j <= end;)
{
tmparr[k++] = arr[j++];
}
//将排序后的辅助数组拷贝到原始数组
for (i = 0; i < (end - start + 1); ++i)
{
arr[start+i] = tmparr[i];
}
}
void MergeSort(int * arr, int start, int end, int *tmparr)
{
if (start < end)
{
int mid = (start + end)/2;
MergeSort(arr, start, mid, tmparr);
MergeSort(arr, mid + 1, end, tmparr);
Merge(arr, start, mid, end, tmparr);
}
}
调用方法:MergeSort(arr, 0, N - 1, tmparr);
非递归的算法如下,SubListSize为合并单元的长度,依次从1,2,4,…2n 增长,直到超过数组的总长度位置。对于从小到大的每个SubListSize,对原数组依次分组处理,每组的长度都为2*SubListSize(末尾不足的除外),这样从短到长一步步合并,最后得到排序后的数组。其优点是没有函数调用栈的开销。
void Mergesort( ElementType A[ ], int N )
{
ElementType *TmpArray;
int SubListSize, Part1Start, Part2Start, Part2End;
TmpArray = malloc( sizeof( ElementType ) * N );
for( SubListSize = 1; SubListSize < N; SubListSize *= 2 )
{
Part1Start = 0;
while( Part1Start + SubListSize < N - 1 )
{
Part2Start = Part1Start + SubListSize;
Part2End = min( N, Part2Start + SubListSize - 1 );
Merge( A, TmpArray, Part1Start, Part2Start, Part2End );
Part1Start = Part2End + 1;
}
}
free(TmpArray);
}
5. 堆排序 Time Complexity:O(Nlog2N) Space Complexity:O(1)
堆排序就是将数组中的元素逻辑上组织成完全二叉树的形式,排序算法核心在于堆的调整。创建堆时,是从后向前构造,即从下往上的根节点构造。调整时,是将对顶元素向下调整。
堆排序时,依次交换堆顶元素和数组末尾元素,然后缩小堆长度并调整堆。如果需要从大到小排序,则建立小顶堆,如果从小到大排序,则建立大顶堆。堆排序是不稳定排序。
下面是从大到小的堆排序算法,设数组有效元素从1开始
void HeapAdjust(int *arr, int r, int m)
{
int rv = arr[r];
int j;
for (j = 2 * r; j <= m; j = 2 * j)
{
if (j < m && arr[j+1] < arr[j])
{
j++;
}
if (arr[j] < rv)
{
arr[r] = arr[j];
r = j;
}
else
break;
}
arr[r] = rv;
}
void HeapSort(int *arr, int N)
{
int i, tmp;
for (i = N/2; i > 0; --i)
{
HeapAdjust(arr, i, N);
}
for (i = N; i > 1; --i)
{
tmp = arr[1];
arr[1] = arr[i];
arr[i] = tmp;
HeapAdjust(arr, 1, i - 1);
}
}
下面是数组元素从0开始的写法:
void HeapAdjust(int *arr, int r, int len)
{
int rv = arr[r];
int j;
for (j = 2 * r + 1; j < len; j = 2 * r + 1)
{
if (j < len - 1 && arr[j+1] < arr[j])
{
j++;
}
if (arr[j] < rv)
{
arr[r] = arr[j];
r = j;
}
else
break;
}
arr[r] = rv;
}
void HeapSort(int *arr, int N)
{
int i, tmp;
for (i = N/2 - 1; i >= 0; --i)
{
HeapAdjust(arr, i, N);
}
for (i = N - 1; i > 0; --i)
{
tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
HeapAdjust(arr, 0, i);
}
}
调用方式都是:HeapSort(arr, N);
6. 快速排序
快速排序是一般情况下使用最多的排序方法,它的平均时间复杂度为O(Nlog2N),虽然在最坏情况下可能达到O(N2),快速排序空间复杂度为O(1),一般使用递归实现,快速排序是一种不稳定的排序算法。
下面是一种经典的实现,从大到小排序:
int Partition(int *arr, int low, int high)
{
int pivot = arr[low];
while(low < high)
{
while(low < high && arr[high] <= pivot) high--;
arr[low] = arr[high];
while(low < high && arr[low] >= pivot) low++;
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
void Qsort(int *arr, int low, int high)
{
if (low < high)
{
int mid = Partition(arr, low, high);
Qsort(arr, low, mid - 1);
Qsort(arr, mid + 1, high);
}
}
一种考虑更全面的实现如下,三数中值分割算法,从小到大排序:
int Median3(int A[], int Left, int Right)
{
int Center = (Left + Right)/2;
if (A[Left] > A[Center])
Swap(&A[Left], &A[Center]);
if (A[Left] > A[Right])
Swap(&A[Left], &A[Right]);
if (A[Center] > A[Right])
Swap(&A[Center], &A[Right]);
//A[Left] <= A[Center] <= A[Right]
Swap(&A[Center], &A[Right - 1]); //Hide pivot
return A[Right - 1]; //Return pivot
}
void Qsort3(int A[], int Left, int Right)
{
int i, j;
int Pivot;
//这里用了一个阈值,当数组长度小于阈值时用简单插入排序,比较高效
if (Left + Cutoff <= Right)
{
Pivot = Median3(A, Left, Right);
i = Left; j = Right - 1;
for ( ; ;)
{
while( A[++i] < Pivot ) {}
while( A[--j] > Pivot ) {}
if (i < j)
Swap(&A[i], &[j]);
else
break;
}
Swap(&A[i], &A[Right - 1]); //Restore pivot
Qsort3(A, Left, i - 1);
Qsort3(A, i + 1, Right);
}
else
InsertSort(A, Left, Right - Left + 1);
}