稳定的排序 | 时间复杂度 | 空间复杂度 |
冒泡排序 | 最差、平均O(n^2),最好O(N) | 1 |
鸡尾酒排序(双向冒泡排序) | 最差、平均O(n^2),最好O(N) | 1 |
插入排序 | 最差、平均O(n^2),最好O(N) | 1 |
归并排序 | 最差、平均、最好O(nlogn) | n |
桶排序 | n | k |
基数排序 | dn | n |
二叉树排序 | nlogn | n |
图书馆排序 | nlogn | (1+e)n |
不稳定的排序 | 时间复杂度 | 空间复杂度 |
选择排序 | 最差、平均n^2 | 1 |
希尔排序 | nlogn | 1 |
堆排序 | 最差、平均、最好O(nlogn) | 1 |
快速排序 | 平均nlogn,最坏n^2 | logn |
快速排序
分而治之,快排最好的情况,选主元每次正好中分,T(N) = O(NLogN),最坏情况,O(n*n)
1.选主元
取头、中、尾的中位数
//选主元,取头、中、尾的中位数
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]);//将pivot藏到右边
//只需考虑A[left+1] ... A[right-2]
return A[right - 1];
}
2.子集划分
特殊情况:如果有元素正好等于pivot,停下来交换。这样的话可以尽可能将两边的子集划分的均衡
在小规模数据直接调用简单排序,大规模数据就用快排
快排的过程如下:
快排就是在一趟排序后,主元到了它该去的位置,即,它左边的数都是比它小的,右边的数都是比它大的
#include<iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
//选主元,取头、中、尾的中位数
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]);//将pivot藏到右边
//只需考虑A[left+1] ... A[right-2]
return A[right - 1];
}
void quick_sort(int A[], int left, int right)
{
if(left >= right)
return;
int pivot = median3(A, left, right);
int i = left;
int j = right - 1;
for(;;)
{
while(i<right-1 && A[++i] < pivot){//注意i的范围
}
while(j>-1 && A[--j] > pivot){//注意j的范围,之前就就是因为i,j的范围没限定,导致一直有错
}
if(i < j)
{
swap(&A[i], &A[j]);
}
else
{
break;
}
}
swap(&A[i], &A[right - 1]);
quick_sort(A, left, i-1 );
quick_sort(A, i+1 , right);
}
int main()
{
int n;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
quick_sort(A, 0, n-1);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
快排的另一种写法(主元是数组第一个数)
#include<iostream>
using namespace std;
int partion(int A[],int low,int high)
{
int pivot = A[low];//用第一个数作为主元
while(low < high)
{
while(low < high && A[high] >= pivot)
--high;
A[low] = A[high];
while(low < high&&A[low] <= pivot)
++low;
A[high] = A[low];
}
A[low] = pivot;
return low;
}
void quick_sort(int A[], int low, int high)
{
if(low < high)
{
int pivot_pos = partion(A, low, high);
quick_sort(A, low, pivot_pos - 1);
quick_sort(A, pivot_pos + 1, high);
}
}
int main()
{
int n;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
quick_sort(A, 0, n - 1);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
冒泡排序
一趟排序后,将最大的元素放到了最后,然后再对最大元素之前的进行同样的操作
最好情况,顺序,T=O(N),最坏情况,逆序,T=O(N*N)
因为冒泡排序中,在前一个元素严格大于后一个元素时才交换,所以他是稳定的。
#include<iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void bubble_sort(int A[], int n)
{
for(int p = n - 1; p >= 0; p--)
{
int flag = 0;//用一个标记,如果没改变的话,说明已经是排好序的,就不用再循环
for(int i = 0; i < p; ++i)
{
if(A[i]>A[i + 1])
{
swap(&A[i], &A[i + 1]);
flag = 1;
}
}
if(0 == flag)
break;
}
}
int main()
{
int n;
cout << "冒泡排序: " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
bubble_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
插入排序
把插入排序想象成摸扑克牌的过程,手里首先有一张牌,A【0】,然后从A[1]开始摸,如果比前一张小,前一张就后移,直到遇到合适的位置,再将这张牌放入
最好情况,顺序,T=O(N),最坏情况,逆序,T=O(N*N)
因为插入排序中,在严格大于时才后移,所以他是稳定的。
#include<iostream>
using namespace std;
void insertion_sort(int A[], int n)
{
for(int p = 1; p < n; p++)
{
int tmp = A[p];//现在摸到的牌,要将他放到它该在的位置
int i = p;
for(; i>0 && A[i - 1] > tmp; i--)
{
A[i] = A[i - 1];
}
A[i] = tmp;
}
}
int main()
{
int n;
cout << "插入排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
insertion_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
希尔排序
希尔排序和插入排序有点像,不同在于插入时比较的数字之间的间隔不一样
比如:81 94 11 96 12 35 17 95 28 58 41 75 15
先每隔5间隔的数做插入排序,那么就是 81 35 41,他们就成了 35 41 81,在数组中就是:
35 94 11 96 12 41 17 95 28 58 81 75 15
再对下一组“5-间隔”数 94 17 75
35 17 11 96 12 41 75 95 28 58 81 94 15
接下来同理,结束后,从【5-间隔】变成【3-间隔】,最后是【1-间隔】
增量序列,Dm > Dm-1 > ... > D1 =1
对每个Dk进行【Dk-间隔】的排序,Dk间隔有序,Dk-1间隔排序后,不会把Dk间隔排好的序给打乱,
原始希尔排序Dm = N/2 (取下界),Dk = (Dk+1) /2,(取下界),最坏时间T = (N^2)
缺陷:增量元素不互质,小增量可能不起作用。
改进:
Hibbard增量序列:Dk = 2^k -1,相邻元素互质,最坏T = (n^1.5),平均时间O(n^1.25)
Sedgewick增量序列:{1,5,9,41,109,。。。} 9* 4^i - 9*2^i + 1 或 4^i - 3*2^i + 1,平均时间(n^7/6),最坏时间O(n^4/3)
#include<iostream>
using namespace std;
void shell_sort(int A[], int n)
{
for(int D = n / 2; D > 0; D = D / 2)//增量序列
{
for(int p = D; p < n; ++p)//插入排序
{
int tmp = A[p];
int i = p;
for(; i >= D && A[i - D]>tmp; i = i - D)//和插入排序不同的地方就是换成了D
{
A[i] = A[i - D];
}
A[i] = tmp;
}
}
}
int main()
{
int n;
cout << "希尔排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
shell_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
归并排序(递归和非递归)
归并排序适用于外排序,它要另外开辟一个空间作为临时的存储,核心在于两个有序子序列的合并。
递归:
#include<iostream>
using namespace std;
//核心,有序子列的归并
void merge(int A[], int tmpA[], int l, int r, int rightend)
{
int leftend = r - 1;
int tmp = l;
int nums = rightend - l + 1;//元素个数
while(l <= leftend && r <= rightend)
{
if(A[l] <= A[r])
tmpA[tmp++] = A[l++];
else
tmpA[tmp++] = A[r++];
}
while(l <= leftend)
tmpA[tmp++] = A[l++];
while(r <= rightend)
tmpA[tmp++] = A[r++];
//把元素拷回A数组
for(int i = 0; i < nums; i++, rightend--)
{
A[rightend] = tmpA[rightend];
}
}
//递归
void Msort(int A[], int tmpA[], int l, int rightend)
{
int center;
if(l < rightend){
center = (l + rightend) / 2;
Msort(A, tmpA, l, center);
Msort(A, tmpA, center+1, rightend);
merge(A, tmpA, l, center + 1, rightend);
}
}
void merge_sort(int A[], int n)
{
int *tmpA;
tmpA =(int*)malloc(n* sizeof(int));
if(tmpA != nullptr)
{
Msort(A, tmpA, 0, n - 1);
free(tmpA);
}
}
int main()
{
int n;
cout << "归并排序(递归) " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
merge_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
非递归:
#include<iostream>
using namespace std;
//和merge不同在不用从tmpA拷回A
void merge1(int A[], int tmpA[], int l, int r, int rightend)
{
int leftend = r - 1;
int tmp = l;
int nums = rightend - l + 1;//元素个数
while(l <= leftend && r <= rightend)
{
if(A[l] <= A[r])
tmpA[tmp++] = A[l++];
else
tmpA[tmp++] = A[r++];
}
while(l <= leftend)
tmpA[tmp++] = A[l++];
while(r <= rightend)
tmpA[tmp++] = A[r++];
}
void merge_pass(int A[], int tmpA[], int n, int length)//length,当前子序列的长度
{
int i = 0;
for(; i <= n - 2 * length; i = i + 2 * length)
merge1(A, tmpA, i, i + length, i + 2 * length - 1);//A元素归并到tmpA后,不拷回A
if(i + length < n)//归并最后两个序列
merge1(A, tmpA, i, i + length, n - 1);
else//只剩一个序列
{
for(int j = i; j < n; ++j)
tmpA[j] = A[j];
}
}
void merge_sort(int A[], int n)
{
int length = 1;
int *tmpA;
tmpA = (int*)malloc(n*sizeof(int));
if(tmpA != nullptr)
{
while(length < n)
{
merge_pass(A, tmpA, n, length);
length *= 2;
merge_pass(tmpA, A, n, length);//刚好归并回A
length *= 2;
}
free(tmpA);
}
}
int main()
{
int n;
cout << "归并排序(非递归) " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
merge_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
选择排序
#include<iostream>
using namespace std;
void select_sort(int A[], int n)
{
for(int i = 0; i < n-1; ++i)
{
int min_index = i;
for(int j = i + 1; j < n; ++j)
{
if(A[j] < A[min_index])
min_index = j;
}
//从未排序的部分中找出值最小的,然后通过交换放到了有序部分的末尾
int temp = A[i];
A[i] = A[min_index];
A[min_index] = temp;
}
}
int main()
{
int n;
cout << "选择排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
select_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
堆排序
首先将数组建成一个最大堆,然后将堆顶和数组末尾交换,使最大的元素到末尾,再对剩下的部分调整成最大堆,再把堆顶交换到末尾,重复这个过程。
#include<iostream>
using namespace std;
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
void adjustheap(int A[], int parent, int n)
{
int child;
int temp = A[parent];//当前要调整的元素
//当前要调整的元素要和他的孩子相比较,但是不能越界,所以就有如下的循环条件
for(; parent*2+1 < n; parent = child)
{
child = parent * 2+1;
//如果有右孩子且右孩子的值大于左孩子
if((child +1 < n) && (A[child]) < A[child + 1])
child++;//找到了左右孩子中更大的
//如果temp更大,那么现在这个位置他是可以“坐稳”的,跳出,否则就将更大的孩子提上来
if(temp >= A[child])
break;
else
A[parent] = A[child];
//一趟结束后,parent就去到更大的孩子的下标的位置
}
A[parent] = temp;
}
void heap_sort(int A[], int n)
{
for(int i = n / 2 - 1; i >= 0; i--)//从最后一个有孩子的结点开始调整,调整出一个最大堆
{
adjustheap(A, i, n);
}
//调整完之后,根结点就是最大值,将最大值交换到数组的最后,然后对剩下的元素又重新调整成最大堆,重复这个过程
for(int i = n - 1; i >= 1; --i)
{
swap(&A[0], &A[i]);
adjustheap(A, 0, i);
}
}
int main()
{
int n;
cout << "堆排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
heap_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}
桶排序
桶排序需要知道输入数据的范围。假设N个学生,成绩0~100,所以建立101个桶,M=101。扫描一遍所有学生的成绩,将它放到对应的桶中。再扫面一遍 桶,输出结果。时间O(M+N)。
#include<iostream>
using namespace std;
//假如是成绩0~100,所以有101个桶
void bucket_sort(int A[], int n)
{
int grade[101] = { 0 };
for(int i = 0; i < n; ++i)
{
grade[A[i]]++;
}
for(int i = 0; i <= 100; ++i)
{
while(grade[i])
{
cout << i << " ";
grade[i]--;
}
}
}
int main()
{
int n;
cout << "桶排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
bucket_sort(A, n);
delete[] A;
return 0;
}
基数排序
当数据范围很大的时候,桶排序就不合适,所以用基数排序。基数排序分为最低位优先和最高位优先,以下代码用最低优先,即,先根据所有数的个位数排序,再对排了序的数根据十位数排序,再对排了序的数根据百位的数值大小排序……
以下代码基数为十进制,最大为三位数
#include<iostream>
using namespace std;
//取一个数的个位,或者十位,或者百位……
//d=1,表示取个位,d=2取十位
int get_digit(int num, int d)
{
int val;
while(d--)
{
val = num % 10;
num = num / 10;
}
return val;
}
void radix_sort(int A[], int n)
{
int radix = 10;//十进制
int *count = new int[radix];//存放对应数位是0,1,2,3...的数的个数
int *bucket = new int[n];//一个桶来存放A数组的数,用于中转
for(int d = 1; d <= 3; ++d)//最大的数有多少位,就要循环多少次,假设最大的数在这里是3位数,所以外层循环3次
{
//初始化为0
for(int i = 0; i < radix; ++i)
count[i] = 0;
//统计数位是0,1,2,3...的数分别有多少个
for(int i = 0; i < n; ++i)
{
int j = get_digit(A[i], d);
count[j]++;
}
//意思是,数位为i的数,在它的前面最多有多少个数
for(int i = 1; i < radix; ++i)
count[i] = count[i] + count[i - 1];
//将数组中的数从后往前装入桶中,保证了稳定性
for(int i = n - 1; i >= 0; --i)
{
int j = get_digit(A[i], d);
bucket[count[j] - 1] = A[i];//根据这个数的数位上是多少,就放到桶的对应的位置
count[j]--;
}
//把桶中的数放回A中,完成一趟排序,下一趟就是比较A数组各个数的十位/百位
for(int i = 0; i < n; ++i)
{
A[i] = bucket[i];
}
}
delete[] count;
delete[] bucket;
}
int main()
{
int n;
cout << "基数排序 " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
radix_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i]<<" ";
cout << endl;
delete[] A;
return 0;
}
鸡尾酒排序(参考“程序员小灰”)
假设一个序列:
这就是鸡尾酒排序的思路。排序过程就像钟摆一样,第一轮从左到右,第二轮从右到左,第三轮再从左到右......
鸡尾酒排序的优点是能够在特定条件下,减少排序的回合数,而缺点是代码量扩大一倍。
适用于【大部分元素已经有序】。
#include<iostream>
using namespace std;
void cock_tail_sort(int A[], int n)
{
int tmp = 0;
for(int i = 0; i < n/2; ++i)
{
//有序的标记,每一轮开始时都为true
bool flag = true;
//奇数轮从左往右进行比较和交换
for(int j = i; j < n - i - 1; ++j)
{
if(A[j]>A[j + 1])
{
tmp = A[j];
A[j] = A[j + 1];
A[j + 1] = tmp;
flag = false;
}
}
if(flag)
break;
//偶数轮,重新标记
flag = true;
//偶数轮,从右往左
for(int j = n - i - 1; j > i; --j)
{
if(A[j] < A[j - 1])
{
tmp = A[j];
A[j] = A[j - 1];
A[j - 1] = tmp;
flag = false;
}
}
if(flag)
break;
}
}
int main()
{
int n;
cout << "鸡尾酒排序: " << endl;
cout << "输入元素个数: ";
cin >> n;
int *A = new int[n];
for(int i = 0; i < n; ++i)
cin >> A[i];
cock_tail_sort(A, n);
for(int i = 0; i < n; ++i)
cout << A[i] << " ";
cout << endl;
delete[] A;
return 0;
}