冒泡排序是一种简单的排序算法。它重复地走访要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作就是重复地进行直到不需要再进行交换为止,也就是说该数列已经排序完成。这个算法的名字由来就是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
复杂度分析
代码实现
void Bubble_Sort(int *arr, int n) {
if(arr == NULL) return;
for(int i = 0; i < n - 1; ++i){
for(int j = 0; j < n - i - 1; ++j){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
对于整体有序的情况如 1 , 2 , 3 , 4 , 5 , 6 , 7 , 9 , 8 , 10 {1,2,3,4,5,6,7,9,8,10} 1,2,3,4,5,6,7,9,8,10 ,显然可能只需要少数的几次交换就可以达到排序后的结果。
所以在某一次遍历的时候,如果发现没有进行任何交换,那么说明现在的序列已经有序了,所以这时我们就可以结束这次排序。
void Bubble_Sort(int *arr, int n) {
if(arr == NULL) return;
for(int i = 0; i < n - 1; ++i){
int flag = 0; // 用来标记是否进行过交换
for(int j = 0; j < n - i - 1; ++j){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = 1;
}
}
if (flag == 0) return; // 若这轮排序没有进行交换,则结束排序
}
}
对于某次交换后,最后一段都已变成有序的情况,显然我们可以跳过后面已经有序的那一段数字的比较。
比 n − i − 1 n - i - 1 n−i−1更快的方式就是记录最后一次进行交换的位置,下一次的遍历只到这里即可
void Bubble_Sort(int *arr, int n) {
if(arr == NULL) return;
int k = n - 1; // 用来记录最后一次交换的位置
for(int i = 0; i < n - 1; ++i){
int flag = 0; // 用来标记是否进行过交换
for(int j = 0; j < k ; ++j){
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = j; // 记录最后一次进行交换的位置
}
}
if (flag == 0) return; // 若这轮排序没有进行交换,则结束排序
k = flag; //否则下一轮的排序只需要遍历到最后一个进行交换的位置即可
}
}
鸡尾酒排序(Cocktail Sort)(又名:双向冒泡排序 (Bidirectional Bubble Sort)、波浪排序 (Ripple Sort)、摇曳排序 (Shuffle Sort)、飞梭排序 (Shuttle Sort) 和欢乐时光排序 (Happy Hour Sort))
与冒泡排序不同的是,此算法双向进行排序,鸡尾酒排序等于是冒泡排序的轻微变形
鸡尾酒排序与冒泡排序的区别在于:鸡尾酒排序每次进行从低到高 然后 从高到低两次排序,而冒泡排序每次都是从低到高去比较序列里的每个元素。
鸡尾酒排序可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只能从一个方向进行比对,每次循环只移动一个元素
void Cocktail_Sort(int *arr,int n){
int left = 0;
int right = n - 1;
while(left < right){
//前半轮,将最大元素放到后面
for(int i = left ; i < right ; ++i)
if(arr[i] > arr[i+1])
swap(arr[i],arr[i+1]);
right--;
//后半轮,将最小元素放到前面
for(int i = right ; i > left ; --i)
if(arr[i] < arr[i-1])
swap(arr[i],arr[i-1]);
left++;
}
}
选择排序是一种简单直观的排序算法。它的工作原理:首先在末排序列中找到最小(大)的元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)的元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法描述
复杂度分析
算法实现
void SelectionSort(int *arr, int n){
if(arr == NULL) return;
for(int i = 0; i < n; ++i){
int minIndex = i;
for(int j = i; j < n; ++j)
if(arr[j] < arr[minIndex])
minIndex = j;
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
插入排序算法是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 i n − p l a c e in-place in−place 排序(即只需用到 O ( 1 ) O(1) O(1) 的额外空间的排序),因而在从后向前扫描的过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法描述
算法分析
代码实现
void InsertionSort(int *arr, int n){
if(arr == NULL) return;
for(int i = 0; i < n - 1; i++){
int currnet = arr[i+1];
int preIndex = i;
while(preIndex >= 0 && currnet < arr[preIndex]){
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = currnet;
}
}
希尔排序是希尔于 1959 1959 1959年 提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破 O ( n 2 ) O(n^2) O(n2) 的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是把记录按一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组所包含的关键词越来越多,当增量减至 1 1 1 时,整个文件恰被分成一组,算法便终止。
算法描述
在此我们选择希尔排序增量为 g a p = l e n g t h / 2 gap = length / 2 gap=length/2,缩小增量继续以 g a p = g a p / 2 gap = gap / 2 gap=gap/2 的方式,这种增量选择我们可以用一个序列来表示, n / 2 , ( n / 2 ) / 2 , . . . , 1 {n/2,(n/2)/2,...,1} n/2,(n/2)/2,...,1 ,称为增量序列。
希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。
算法分析
代码实现
void Shell_Sort(int *arr, int n){
if(arr == NULL) return;
int temp, gap = n / 2;
while(gap > 0){
for(int i = gap; i < n; i++){
temp = arr[i];
int preIndex = i - gap;
while(preIndex >= 0 && arr[preIndex] > temp){
arr[preIndex + gap] = arr[preIndex];
preIndex -= gap;
}
arr[preIndex + gap] = temp;
}
gap /= 2;
}
}
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,始终都是 O ( n l o g n ) O(nlogn) O(nlogn) 的时间复杂度。代价是需要额外的内存空间
归并排序是建立在归并操作上的一种有效的排序算法。该算法是分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使得每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述
复杂度分析
代码实现
void Merge(int arr[], int p, int q, int r){
int n1 = q - p + 1;
int n2 = r - q;
int *L;
L = (int*)malloc(sizeof(int) * n1);
int *R;
R = (int*)malloc(sizeof(int) * n2);
int i = 0;
for(; i < n1; ++i) L[i] = arr[i + p];
int j = 0;
for(; j < n2; ++j) R[j] = arr[j + q + 1];
i = j = 0;
int k = p;
while(i != n1 && j != n2){
if(L[i] <= R[j])
arr[k++] = L[i++];
else
arr[k++] = R[j++];
}
while(i < n1) arr[k++] = L[i++];
while(j < n2) arr[k++] = R[j++];
free(L);free(R);
}
void MergeSort(int arr[], int p, int q){
if(p < q){
int r = (p + q) / 2;
MergeSort(arr, p, r);
MergeSort(arr, r + 1, q);
Merge(arr,p, r, q);
}
}
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
在C++的STL中,sort采用的就是快速排序,并且大致加入了下面三种优化。
快速排序是在原序列越有序时越慢,越无序则越快
算法描述
复杂度分析
代码实现
int partition(int *a, int left, int right){
int key = a[left];
while(left < right){
while(left < right && a[right] >= key) right--;
a[left] = a[right];
while(left < right && a[left] <= key) left++;
a[right] = a[left];
}
a[left] = key;
return left;
}
void Qsort(int *a, int left, int right){
if(left < right){
int pos = partition(a, left, right);
Qsort(a, left, pos - 1);
Qsort(a, pos + 1, right);
}
}
随机化快排 / 三数取中 / 取中间值 等优化,都是基于选择的"基准"不同来达到提速的效果。
因为快速排序在排序一个已经有序的序列时,我们会发现每次分区的时候两个区的长度分别为 1 1 1 和 n − 1 n-1 n−1 ,这样的话就会导致 O ( n 2 ) O(n^2) O(n2) 的复杂度,但是实际上如果我们对于一个有序的序列,每次选中间值的话,就可以均分这两段区间。
所以这类优化的思路都是基于选择的"基准"不同来使划分区间的时候尽可能平均。
int partition(int *a, int left, int right){
//int tmp = rand() % (right - left + 1) + left;
int tmp = (right + left) >> 1;
swap(a[left],a[tmp]);
int key = a[left];
while(left < right){
while(left < right && a[right] >= key) right--;
a[left] = a[right];
while(left < right && a[left] <= key) left++;
a[right] = a[left];
}
a[left] = key;
return left;
}
void Qsort(int *a, int left, int right){
if(left < right){
int pos = partition(a, left, right);
Qsort(a, left, pos - 1);
Qsort(a, pos + 1, right);
}
}
显然在大数据量的排序下,会出现很多重复的数据,这时候,所有跟"基准"相等的元素并没有必要再进入之后的分区,所以这时我们可以将区间分成三段,而不是之前的两段。
排序的时候依旧递归左右两个区间,但是中间与基准相等的这一段区间显然没有必要再继续进行递归。
void QSort3Ways(int *arr, int l, int r) {
if (r <= l) return;
int temp = arr[l];
int lt = l; // arr[l+1...lt] < pivot
int gt = r + 1; // arr[gt...r] > pivot
int i = l + 1; // arr[lt+1...i) == pivot
while (i < gt) {
if (arr[i] < temp) {
swap(arr[i], arr[lt + 1]);
i++;
lt++;
} else if (arr[i] > temp) {
swap(arr[i], arr[gt - 1]);
gt--;
} else {
i++;
}
}
swap(arr[l], arr[lt]);
QSort3Ways(arr, l, lt - 1);
QSort3Ways(arr, gt, r);
}
在子序列比较小的时候,其实插入排序是比较快的,因为对于有序的序列,插排可以达到 O ( n ) O(n) O(n) 的复杂度,如果序列比较小,则和大序列比起来会更容易有序,这时候使用插入排序效率要比快速排序高。
实现方法也很简单:快排是在子序列元素个数变成 1 1 1 时,才停止递归,我们可以设置一个阈值,当长度小于一个固定的数值时使用插入排序,否则继续递归使用快速排序 (在很多论文中会使用 7 7 7 作为这个阈值,在大量数据测试下, 7 7 7 对整体代码的速度提升是最多的)
void QSort3Ways(int *arr, int l, int r) {
if (r - l <= 7) {
InsertSort(arr, l, r);
return;
}
swap(arr[l],arr[rand()%(r-l+1) + l]);
int temp = arr[l];
int lt = l; // arr[l+1...lt] < pivot
int gt = r + 1; // arr[gt...r] > pivot
int i = l + 1; // arr[lt+1...i) == pivot
while (i < gt) {
if (arr[i] < temp) {
swap(arr[i], arr[lt + 1]);
i++;
lt++;
} else if (arr[i] > temp) {
swap(arr[i], arr[gt - 1]);
gt--;
} else {
i++;
}
}
swap(arr[l], arr[lt]);
QSort3Ways(arr, l, lt - 1);
QSort3Ways(arr, gt, r);
}
堆排序是指利用堆这种数据结构所涉及的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足下面的性质:子结点的键值或索引总是小于(或者大于)它的父结点。
堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点,当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。
算法描述
复杂度分析
代码实现
void downAdjust(int *arr,int low, int high) {
//low表示最低的元素下标,high表示数组的最后一个元素的下标
int current = low, lchild = current * 2;
//lchild表示左孩子
while (lchild < high) {//如果左孩子存在
//如果右孩子存在,且右孩子的值大于当前结点值
if (lchild + 1 < high && arr[lchild] < arr[lchild + 1]) {
lchild = lchild + 1; //改成右节点
}
if (arr[lchild] > arr[current]) {
swap(arr[lchild], arr[current]);
current = lchild;
lchild = current * 2;
}
else {
break;
}
}
}
void HeapSort(int *arr,int n) {
for (int i = n / 2; i >= 0 ; --i)
downAdjust(arr,i, n);
for (int i = n - 1; i > 0; --i) {
swap(arr[0], arr[i]);
downAdjust(arr,0, i);
}
}
算法描述
复杂度分析
代码实现
void Bucket_sort(int *arr,int n){
int a[10000];
for(int i = 0 ; i < n ; ++i) //循环读入5个数
{
scanf("%d",&t); //把每一个数读到变量t中
a[t]++; //进行计数
}
for(int i = 0 ; i <= 10 ; ++i) //依次判断a[0]~a[10]
for(int j = 1 ; j <= a[i] ; ++j) //出现了几次就打印几次
printf("%d ",i);
}
多线程睡眠排序…开个玩笑,这是个有趣的算法
void sleepSortHelper(int i) {
this_thread::sleep_for(chrono::microseconds(i*10000));
std::cout << i << " ";
}
void sleepSort(int *arr,int size) {
for (int i = 0; i < size; i++)
{
std::thread t(sleepSortHelper,arr[i]);
t.detach();
}
}
void main() {
sleepSort(arr, 7);
getchar();
}
各种排序的稳定性,时间复杂度和空间复杂度总结:
我们比较时间复杂度函数的情况:对 n n n 较大的排序记录。一般的选择都是时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 的排序方法。
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对顺序保持不变,则称该算法是稳定的;若经排序后,记录的相对顺序发生了改变,则称该算法是不稳定的。
稳定性的好处:排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。另外,如果排序算法稳定,可以避免多余的比较;