2.5 排序算法

143 阅读9分钟

基本概念

排序:就是表内元素有序化的过程。

算法的稳定性:就是相同的数字的排列顺序,在排序前后,都不会发生变化,比如3 5 1 0 3 2 9,很明显第一个3是红色的,第二个3是黑色的,排序过后结果为0 1 2 3 3 5 9,而不是0 1 2 3 3 5 9。前后排列顺序不会发生变化。

排序算法的分类:

  • 内部排序:数据都在内存中,关注如何使算法的时间/空间复杂度更低。
  • 外部排序:数据太多,无法全部放入内存,除了关注时间/空间复杂度,还要关注如何使读/写磁盘次数更低。

插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。其算法思想就是每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。用链表和数组都可实现。

数组实现:

#include <iostream>
#include<vector>
using namespace std;
void insert_sort(vector<int> &num){
int pre,cur;
for(int i = 0;i < num.size();i++){
	pre = i  - 1;
	cur = num[i];
	while(pre >= 0&& num[pre] > cur){
		num[pre + 1] = num[pre];
		pre--;
	}
	num[pre + 1] = cur;
}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	insert_sort(num);
	for(auto n:num) cout<<n<<endl;
	return 0;
}

链表实现:

#include <iostream>
#include<vector>
using namespace std;
struct Node{
int num;
Node* next;	
};
void insert_sort(Node* first){
Node* tmp = first->next;
Node* pre = first;
while(tmp != nullptr){
	Node* left = first;
	Node* right = tmp;
	Node* l_copy = left;
	while(left != right && left->num <= right->num) l_copy = left,left = left->next;
	if(left != right){
		pre->next = right->next;
		l_copy -> next = right;
		right->next = left;
	}
	pre = tmp;
	tmp = tmp->next;
}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	Node* first = new Node();
	Node* tmp = first;
	for(int n:num){
		Node* node = new Node();
		node->num = n;
		node->next = tmp->next;
		tmp->next = node;
		tmp = tmp->next;
	}
	insert_sort(first);
	while(first->next != nullptr) cout<<first->next->num,first = first->next;
	return 0;
}

算法效率分析:

  • 空间复杂度:O(1)O(1)

  • 时间复杂度:主要来自对比关键字,移动元素。若有n个元素,则需n-1趟处理。

  • 最好情况:原本就有序,每一趟只需要对比,不用移动,时间复杂度O(n)O(n)

  • 最坏情况:原本为逆序,对比i次需要移动i+1次。时间复杂度为O(n2)O(n^2)

当然插入排序的插入可以用二分查找找合适的值,这样对算法进行优化:

#include <iostream>
#include<vector>
using namespace std;
void insert_sort(vector<int> &num){
int pre,cur;
for(int i = 0;i < num.size();i++){
	pre = i  - 1;
	cur = num[i];
	int high = i,low = 0,mid;
	while(low <= high){
		mid = (low + high) >> 1;
		if(cur > num[mid]) low = mid + 1;
		else high = mid - 1;
	}
	while(pre >= 0&&pre >high) num[pre+1] = num[pre],pre--;
	num[pre + 1] = cur;
}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	insert_sort(num);
	for(auto n:num) cout<<n<<endl;
	return 0;
}

选择排序

选择排序(Selection sort)是一种简单直观的排序算法。其基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法性能分析:

  • 空间复杂度:O(1)O(1)
  • 时间复杂度:O(n2)O(n^2)
  • 稳定性:不稳定。
  • 适用性:既适用顺序表,又适应链表。

数组实现:

#include <iostream>
#include<vector>
using namespace std;
void select_sort(vector<int> &num){
	for(int i=0; i<num.size();i++){
		int little = i;
		for(int j = i + 1;j < num.size();j++){
			if(num[little] > num[j]) little = j;
		}
		int temp = num[i];
		num[i] = num[little];
		num[little] = temp;
	}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	select_sort(num);
	for(auto n:num) cout<<n<<endl;
	return 0;
}

链表实现:

#include <iostream>
#include<vector>
using namespace std;
struct Node{
int num;
Node* next;	
};
void select_sort(Node* first){
    Node* head = first->next;
	Node* target;
	while(head != nullptr){
		target = head;
		Node* min = target;
		while(target != nullptr){
			if(min->num > target->num) min = target;
			target = target->next;
		}
		int temp = head->num;
		head->num = min->num;
		min->num = temp;
		head = head->next;
	}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	Node* first = new Node();
	Node* tmp = first;
	for(int n:num){
		Node* node = new Node();
		node->num = n;
		node->next = tmp->next;
		tmp->next = node;
		tmp = tmp->next;
	}
	select_sort(first);
	while(first->next != nullptr) cout<<first->next->num,first = first->next;
	return 0;
}

冒泡排序

冒泡排序要对一个列表多次重复遍历。它要比较相邻的两项,并且交换顺序排错的项。每对 列表实行一次遍历,就有一个最大项排在了正确的位置。大体上讲,列表的每一个数据项都会在 其相应的位置 “冒泡”。如果列表有 n 项,第一次遍历就要比较 n-1 对数据。需要注意,一旦列 表中最大(按照规定的原则定义大小)的数据是所比较的数据对中的一个,它就会沿着列表一直 后移,直到这次遍历结束。

算法性能分析:

  • 空间复杂度:O(1)O(1)
  • 时间复杂度:O(n2)O(n^2)
  • 稳定性:稳定。
  • 适用性:既适用顺序表,又适应链表。

数组实现:

#include <iostream>
#include<vector>
using namespace std;

void bubble_sort(vector<int>& num){
for(int i=0; i < num.size();i++)
	for(int j = 0;j < num.size() - i - 1;j++)
		if(num[j] > num[j + 1]) {
			int temp = num[j];
			num[j] = num[j+1];
			num[j+1] = temp;
		}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	bubble_sort(num);
	for(int n:num) cout<<n<<endl;
	return 0;
}

链表实现:

#include <iostream>
#include<vector>
using namespace std;
struct Node{
int num;
Node* next;	
};
void bubble_sort(Node* first,int n){
for(int i=0;i<n;i++){
   Node* left = first->next;
   int m = n - i;
   while(left->next!=nullptr && m >= 0){
	m--;
	if(left->next->num < left->num){
		int tmp = left->num;
		left->num = left->next->num;
		left->next->num = tmp;
	} 
	left = left->next;
   }
}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	Node* first = new Node();
	Node* tmp = first;
	for(int n:num){
		Node* node = new Node();
		node->num = n;
		node->next = tmp->next;
		tmp->next = node;
		tmp = tmp->next;
	}
	bubble_sort(first,num.size());
	while(first->next != nullptr) cout<<first->next->num,first = first->next;
	return 0;
}

希尔排序

希尔排序有时又叫做 “缩小间隔排序”,它以插入排序为基础,将原来要排序的列表划分为一些子列表,再对每一个子列表执行插入排序,从而实现对插入排序性能的改进。划分子列的特定方法是希尔排序的关键。我们并不是将原始列表分成含有连续元素的子列,而是确定一个划分列表的增量 “i”,这个i更准确地说,是划分的间隔。然后把每间隔为i的所有元素选出来组成子列表,然后对每个子序列进行插入排序,最后当 i=1 时,对整体进行一次直接插入排序。

算法性能分析:

  • 空间复杂度:O(1)O(1)
  • 时间复杂度:优于直接插入排序。
  • 稳定性:不稳定。
  • 适用性:只用顺序表。
#include <iostream>
#include<vector>
using namespace std;
void shell_sort(vector<int> &num){
	int n = num.size();
	for(int d = n/2;d > 0;d = d/2)
		for(int j = d;j < n;j++)
			if(num[j]<num[j-d]){
			int tmp = num[j];
			int k = j - d;
			for(;k >= 0&&tmp < num[k];k -= d) num[k + d] = num[k];
			num[k+d] = tmp;
		        }
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	shell_sort(num);
	for(int n:num) cout<<n;
	return 0;
}

归并排序

归并排序是一种递归算法,如果看成二路归并,它持续地将一个列表分成两半。如果列表是空的或者 只有一个元素,那么根据定义,它就被排序好了(最基本的情况)。如果列表里的元素超过一个,我们就把列表拆分,然后分别对两个部分调用递归排序。一旦这两个部分被排序好了,然后就可以对这两部分数列进行归并了。归并是这样一个过程:把两个排序好了的列表结合在一起组合成一个单一的有序的新列表。有自顶向下(递归法)和自底向上的两种实现方法。

同理如果是k路归并,会被看成k个。

算法性能分析:

  • 空间复杂度:O(n)O(n)
  • 时间复杂度:O(nlog2n)O(nlog_2 n)
  • 稳定性:稳定。
#include <iostream>
#include<vector>
using namespace std;
void merge_sort(vector<int> &num,int low,int high){
	if(low < high){
		int mid = (low + high) >> 1;
		merge_sort(num,low,mid);
		merge_sort(num,mid+1,high);
		vector<int> N = num;
		int i,j,k;
		for(i = low,j = mid+1,k = low;i <= mid&&j <= high;k++){
			if(N[i] > N[j]) num[k] = N[j++];
			else num[k] = N[i++];
		}
		while(i <= mid) num[k++] = N[i++];
		while(j <= high) num[k++] = N[j++];
	}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	merge_sort(num,0,num.size() - 1);
	for(int n:num) cout<<n;
	return 0;
}

快速排序

快速排序由 C. A. R. Hoare 在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

步骤:

  • 从数列中挑出一个元素,称为"基准"(pivot)。
  • 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
#include <iostream>
#include<vector>
using namespace std;
void quick_sort(vector<int> &num,int low,int high){
	int cpl = low;
	int cph = high;
	int pivot = num[low];
	if(low > high) return ;
	while(low < high&&num[high] >= pivot) high--;
	num[low] = num[high];
	while(low < high&&num[low] <= pivot) low++;
	num[high] = num[low];
	num[low] = pivot;
	quick_sort(num,cpl,low-1);
	quick_sort(num,low+1,cph);
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	quick_sort(num,0,num.size() - 1);
	for(int n:num) cout<<n;
	return 0;
}

奇偶排序

奇偶排序又叫砖排序,是一种相对简单的算法,最初发明用于有本地互联的并行计算。在具有多处理器的环境中很有用,处理器可以分别处理每一个奇数对,然后又同时处理偶数对。因为奇数对是彼此独立的,每一刻都可以用不同的处理器并行处理比较和交换,实现高速排序。

算法思想

  1. 选取所有的奇数列的元素,与其右边的相邻元素进行比较,将较小的元素排序在前面
  2. 选取所有的偶数列的元素,与其右边的相邻元素进行比较,将较小的元素排序在前面
  3. 重复前面的步骤,直到所有的序列有序为止
#include <iostream>
#include<vector>
using namespace std;
void OddEven_sort(vector<int> &num){
	bool already = false;
	while(!already){
		already = true;
		for(int i = 0;i < num.size();i += 2)
			if(i + 1 < num.size()&&num[i+1] < num[i]){
				int tmp = num[i];
				num[i] = num[i+1];
				num[i+1] = tmp;
				already = false;
			}
		for(int i = 1;i < num.size();i += 2)
		  	if(i + 1 < num.size()&&num[i+1] < num[i]){
				int tmp = num[i];
				num[i] = num[i+1];
				num[i+1] = tmp;
				already = false;
			}
	}
}
int main() {
	vector<int> num = {1,9,3,4,6,8,9,2,4};
	OddEven_sort(num);
	for(int n:num) cout<<n;
	return 0;
}

基数排序

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。

具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

 

基数排序图文说明

通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:

在上图中,首先将所有待比较树脂统一为统一位数长度,接着从最低位开始,依次进行排序。

  1. 按照个位数进行排序。
  2. 按照十位数进行排序。
  3. 按照百位数进行排序。

排序后,数列就变成了一个有序序列。

算法性能分析:(有d位,有r个数)

  • 空间复杂度:O(r)O(r)
  • 时间复杂度:O(d(n+r))O(d(n+r))
  • 稳定性:稳定。
#include <iostream>
#include<vector>
#include<cmath>
using namespace std;
void radix_sort(vector<int>& a){
	int d = 0,max = 0;
	for(int ai:a){
		if(ai > max) max = ai,d = 1+log10(ai);
	}
	vector<vector<int>> dd; 
	for(int i = 0;i < d;i++){
		for(int j = 0;j < a.size();j++){
			for(int k = 0;k < a.size() - j - 1;k++){
				int a1 = (int)a[k]/ pow(10, i+1);
				int a2 = (int)a[k+1]/ pow(10, i+1);
				a1 = a1 % 10;
				a2 = a2 % 10;
				if(a2 < a1) {
					int tmp = a[k];
					a[k] = a[k+1];
					a[k+1] = tmp;
				}
			}
		}
	}
}
int main() {
	vector<int> num = {213,42,234,52,12,545,124};
	radix_sort(num);
	for(int n:num) cout<<n<<endl;
	return 0;
}

木桶排序

木桶排序(箱排序)就是将数组中的数分到有限个木桶中去。木桶排序的必要步骤是计数,记录每一个数出现的次数。

原理图展示:

image.png

我们先假设待排数组是a,数组长度为n,max代表数组中最大的数所在的范围,即[ 0,max)。木桶数组为buceks。

看上面的图,我们可以看到,在循环的时候,buckes[a[ i ]]++。这样就统计了每个数在a数组中出现的次数,并记录在了木桶数组中的相应位置。

我们计数之后,就要进行排序,排序我们就只需要遍历木桶数组,只要木桶数组的元素不为0,那么就代表这个数出现过。并且木桶数组的下标就是就是这个数,

木桶数组对应下标的值就是这个元素出现的次数。

#include <iostream>
#include<vector>
using namespace std;
void bucket_sort(vector<int>& a){
	int max = 0;
	for(int ai:a) if(ai > max) max = ai;
	vector<int> b(max+1,0);
	for(int ai:a) b[ai]++;
	int j = 0;
	for(int i = 0;i < max+1;i++){
		while(b[i] != 0) a[j++] = i,b[i]--;
	}
}
int main() {
	vector<int> num = {0,2,5,8,2,5,8,2,6,9,1};
	bucket_sort(num);
	for(int n:num) cout<<n<<endl;
	return 0;
}

鸡尾酒排序

鸡尾酒排序是一种定向的冒泡排序,也可以称为搅拌排序、涟漪排序。是冒泡排序的一种变形。和冒泡排序的区别在于,鸡尾酒排序采用了双向比较并替换的原理

实现:

  1. 声明两个临时指针left和right,分别指向第一个元素和最后一个元素。
  2. 每一轮比较时,从right往left方向查找最大数,放到right位置,从left往right方向查找最小数,放到left位置。每查找完一遍之后,因为最大数和最小数位置已经确定好,所以把查找边界缩短,如left++、right--。
  3. 重复第二步操作,直到left和right相遇时,表示排序已完成了。

图解:

image.png

#include <iostream>
#include<vector>
using namespace std;
void cocktail_sort(vector<int>& num){
	int left = 0,right = num.size() - 1;
	while(left < right){
		for(int i = right;i > 0;i--)
			if(num[i] > num[i+1]){
				int tmp = num[i];
				num[i] = num[i+1];
				num[i+1] = tmp;
			}
		left++;
		for(int i = left;i < right;i++)
			if(num[i] > num[i+1]){
				int tmp = num[i];
				num[i] = num[i+1];
				num[i+1] = tmp;
			}
		right--;
	}
}
int main() {
	vector<int> num = {0,2,5,8,2,5,8,2,6,9,1};
	cocktail_sort(num);
	for(int n:num) cout<<n<<endl;
	return 0;
}

地精排序

Gnome排序(地精排序),起初由Hamid Sarbazi-Azad 于2000年提出,并被称为stupid排序,后来被Dick Grune描述并命名为“地精排序”,作为一个排序算法,和插入排序类似,除了移动一个元素到最终的位置,是通过交换一系列的元素实现,就像冒泡排序一样。概念上十分简单,不需要嵌套循环。时间复杂度为O(n2)O(n^2),但是如果初始数列基本有序,时间复杂度将降为O(n)O(n)。实际上Gnome算法可以和插入排序算法一样快。平均运行时间为O(n2)O(n^2).

Gnome排序算法总是查找最开始逆序的一对相邻数,并交换位置,基于交换两元素后将引入一个新的相邻逆序对,并没有假定当前位置之后的元素已经有序.

其实就是如果前后发生交换,就交换以后,检查的位置返回上一步,重新看。

实例:

给定一个无序数组, a = [5, 3, 2, 4], gnome排序将在while循环中执行如下的步骤. "pos"采用加粗黑体:

当前数组操作
[5, 3, 2, 4]a[pos] < a[pos-1], 交换
[3, 5, 2, 4]a[pos] >= a[pos-1], pos自增
[3, 5, 2, 4]a[pos] < a[pos-1], 交换并且pos > 1, pos自减
[3, 2, 5, 4]a[pos] < a[pos-1], 交换并且pos <= 1, pos自增
[2, 3, 5, 4]a[pos] >= a[pos-1], pos自增
[2, 3, 5, 4]a[pos] < a[pos-1], 交换并且pos > 1, pos自减
[2, 3, 4, 5]a[pos] >= a[pos-1], pos自增
[2, 3, 4, 5]a[pos] >= a[pos-1], pos自增
[2, 3, 4, 5]pos == length(a), 完成
#include <iostream>
#include<vector>
using namespace std;
void gnome_sort(vector<int>& num){
	int pos = 1;
	while(pos < num.size()){
		if(num[pos] >= num[pos - 1]) pos++;
		else{
			int tmp = num[pos];
			num[pos] = num[pos - 1];
			num[pos-1] = tmp;
			if(pos > 1) pos--;
		}
	}
}
int main() {
	vector<int> num = {0,2,5,8,2,5,8,2,6,9,1};
	gnome_sort(num);
	for(int n:num) cout<<n<<endl;
	return 0;
}

堆排序

算法描述:

堆排序按照从小到大的顺序进行排序的步骤如下:

  1. 将长度为 n 的序列构造成为一个大顶堆

  2. 将根结点与序列最后一个结点进行交换,此时最后一个结点就是该序列的最大值

  3. 将剩余的 n - 1 个结点再次构造成为一个大顶堆;

  4. 重复步骤 2、3, 直到构造成一个完全有序的序列。

堆需要满足两个条件:

  1. 是一棵完全二叉树(Complete Binary Tree)

  2. 堆上面的每一个节点都必须满足父结点的值大于等于子结点的值或者父结点的值小于等于子结点的值 。parent >= children or parent <= children

完全二叉树:

一棵深度为 k 的有 n 个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为 i(1≤i≤n)的结点与满二叉树中编号为 i 的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

举个例子:

满二叉树:

深度为 k 且有 2^k-1 个结点的二叉树称为满二叉树。

image.png

完全二叉树:

树中含有 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。

image.png

大顶堆和小顶堆:

堆又分为大顶堆和小顶堆。

大顶堆:

完全二叉树且每个父节点都大于等于它的两个子结点时,就称为大顶堆

公式定义:

arr[i]>=arr[2i+1] arr[i] >= arr[2i+1]

arr[i]>=arr[2i+2]arr[i] >= arr[2i+2]

image.png 小顶堆:

完全二叉树且每个父节点都小于等于它的两个子结点时,就称为小顶堆

公式定义:

arr[i]<=arr[2i+1]arr[i] <= arr[2i+1]

arr[i]<=arr[2i+2]arr[i] <= arr[2i+2]

image.png

堆的性质:

例如我们已经有一个大顶堆,那么这个大顶堆具有哪些特征呢?

image.png 将其映射为数组:

intarr[]=12,10,9,6,7,8,5,3,2int arr[] = {12,10,9,6,7,8,5,3,2}

image.png

我们假设结点 6 的位置为 i = 3。那么我们可以求出,他的父节点和两个子结点

Parent = ( i - 1 ) / 2,等于位置为 1 的结点 10。

C1 = 2i + 1,等于位置为 7 的结点 3 。

C2 = 2i + 2,等于位置为 8 的结点 2 。

图解堆排序算法:

我们以 int tree[] = {6, 10, 3, 8, 5, 12, 7, 2, 9} 为例,对堆排序的执行过程进行图解。

第一步: 通过原始序列构造一个大顶堆(升序采用大顶堆,降序采用小顶堆)。

  1. 先通过序列通过从上到下从左往右的顺序构造一个完全二叉树

image.png 2. 对第三层的父节点和第四层的子结点进行调整,其中粉红色代表发生交换的结点。使得其父节点大于两个子结点。

即也是从最后 1 个非叶子结点开始 (8 - 1) / 2 = 3, 也就是从 8 开始进行从左到右从下到上进行调整。

[8,2,9] 中,9 最大,交换 8 和 9。

image.png

  1. 对第二层父节点和第三层子结点进行调整。

[10,9,5] 中,10 最大,不交换。

[3,12,7] 中,12 最大,交换 3 和 12。

image.png

  1. 对第一层父节点和第二层子结点进行调整。

[6,10,12] 中,12 最大,交换 6 和 12。

image.png

这时,上面操作导致 [6,3,7] 不是一个大顶堆,继续调整。

[6,3,7] 中,7 最大,交换 6 和 7。

image.png 至此,大顶堆构建完成。

**第二步: ******将根结点调整到最后一个位置,使末尾元素最大。然后对剩下元素继续调整堆,根结点调整到最后一个位置,得到第二大元素。如此反复进行交换、重够、交换。

  1. 将根节点元素 12 跟 8 交换。

image.png 2. 重新调整结构,使其继续满足堆定义。

image.png

  1. 将根节点元素 10 跟 2 交换。

image.png

  1. 重新调整结构,使其继续满足堆定义。

image.png

这样一直交换、重够、交换下去.......

5.最后一次将根节点元素 3 跟 2 交换。

image.png

这样所有元素就达到了有序状态。

算法分析:

时间复杂度:堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)

空间复杂度: 因为堆排序是就地排序,空间复杂度为常数:O(1)****

稳定性: 不稳定。因为在堆的调整过程中,元素进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的元素就可能出现排在后面的元素被交换到前面来的情况。

#include <iostream>
#include<vector>
using namespace std;
void heap_fi(vector<int> &num,int n,int i){
	if(i >= n) return;
	int lc = i *2 + 1;
	int rc = i* 2 + 2;
	int max = i;
	if(lc < n && num[lc] > num[max]) max = lc;
	if(rc < n && num[rc] > num[max]) max = rc;
	if(max != i){
		swap(num[max],num[i]);
		heap_fi(num,n,max);
	}
}

void build_heap(vector<int> &num,int n){
	int parent = (n - 2) >> 1;
	for(int i = parent;i >= 0;i--) heap_fi(num,n,i);
}
void heap_sort(vector<int> & num){
	int n = num.size();
	build_heap(num,n);
	for(int i = n - 1;i >=0;i--){
		swap(num[i],num[0]);
		heap_fi(num,i,0);
	}
}
int main() {
	vector<int> num = {0,2,5,8,2,5,8,2,6,9,1};
	heap_sort(num);
	for(int n:num) cout<<n;
	return 0;
}

锦标赛排序

生活中的淘汰锦标赛:在单淘汰的锦标赛中,选手们两两比赛,胜者晋级,败者被淘汰。比如世界乒乓球锦标赛或者大满贯网球赛就是这么进行的。
这样一来,就可以把比赛的赛程和结果对应成一个二叉树。在树中每一个选手是二叉树中的一个叶子结点,每一场比赛就相当于两个数字在比大小,数字大的选手获胜进入下一轮,成为树干上的根。所以,进入到某一轮比赛的选手,其实都是某个子数干的根结点。最后的冠军就是整个二叉树的根结点。这种赛制的合理性需要一个假设:A>B, B>C --> 必然有A>C(输赢的传递性)

image.png

锦标赛排序法:对所有数字排序,复杂度是nlogn(和快速排序差不多)。特定的场合,它更快,如果只选第一名,则算法复杂度只有N,若需要选出第二名,则额外增加logn就可以了,对第三名也是如此。这种方法在从N个选手中选出K个选手的事情中特别快

工程中,要比较两个数字的大小
第一步:把所有的数字放到二叉树的叶子节点,然后按照锦标赛单淘汰的方式,两两比较选出最大
第二步:对于第二大的,从所有被最大的数字淘汰的数字中选择,以此类推选择对于第三、第四大的数字

#include <iostream>
#include<vector>
using namespace std;
int champion_fi(vector<int> &num,int n){
	int d = n,m = 0;
	while(d > 1){
		m += d;
		if(d%2 == 1) d = d/2 + 1;
		else d = d/2;
	}
	vector<int> k(m+1,0);
	for(int i = m + 1 - n;i <= m;i++){
		k[i] = i - m - 1 + n;
	}
	for(int i = m - n;i >= 0;i--){
		int lc = i*2 + 1;
		int rc = i*2 + 2;
		if(rc > m) k[i] = k[lc];
		else if(num[k[lc]] > num[k[rc]]) k[i] = k[lc];
		else k[i] = k[rc];
	}
	//找到最大的,开始换。
	return k[0];
}
void tournament_sort(vector<int> &num){
	for(int i = num.size();i > 0 ;i--){
		int large = champion_fi(num,i);
		int tmp = num[i - 1];
		num[i - 1] = num[large];
		num[large] = tmp;
	}
}
int main() {
	vector<int> num = {2,6,3,8,7,9,1};
	tournament_sort(num);
	for(int i = 0; i < num.size();i++) cout<<num[i]<<endl;
	return 0;
}

梳子排序

梳排序也属于简单的排序算法,也是一种不稳定的排序方法。在讲述梳排序的思想之前,先介绍一下与该排序有关的一个关键常量:递减率;该常量为固定值:1.3。该常量是由原作者Wlodzimierz Dobosiewicz以随机数做实验得到的,对于此大可不必纠结,直接用就行。

(1) 梳排序的思想

  梳排序的思想就是:最开始的时候先定义一个步长,该步长为数组的长度除以1.3(注意:直接用整型除即可,例如数组长度为5,则开始的步长为5/1.3=3),然后从第0个元素开始,跟距离当前元素为步长大小的元素进行比较(例如,开始步长为3,所以第0个元素跟第3个元素进行比较),若前者大于后者就进行交换,否则继续往后进行比较,直到比较到最后一个元素为止;完成前面的操作后,第一步就算完成了,接下来从第二步开始,将第一步的步长除以1.3(第一步的例子中步长为3,故第二步的步长为3/1.3=2)后得到新的步长,之后参照第一步的做法进行比较,直到比较到最后一个元素为止;接下来讲述一下约束条件,当步长小于1的时候,不再进行比较操作。此外,稍微提一下,该例子以排升序为例,排降序与其类似,读者参照后可自行编写。梳排序最后一次排序是冒泡排序。

  所以,从上述的思想中,我们知道:递减率其实就是所要比较的两个元素的间距的递减率。为了更好地理解该算法的思想,我们来看以下图片:

image.png

从上图可以知道,原始数组为:41 11 7 16 25 4 23 32 31 22 9 1 22 3 7 31 6 10,3以排升序为例,接下来进行分步操作。  

第一步:数组长度为19,故第一步的步长为19/1.3=14。故第0个元素41与第14个元素3进行比较,41大于3,进行交换;同理,第1个元素11与第15个元素7进行比较,11大于7,进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 7 18 6 10 25 4 23 32 31 22 9 1 22 41 11 31 7 16。

第二步:第一步的步长为14,故第二步的步长为14/1.3=10。故第0个元素3与第10个元素22进行比较,3小于10,不进行交换;同理,第1个元素7与第11个元素9进行比较,7小于9,不进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 7 1 6 10 11 4 7 16 31 22 9 18 22 41 25 31 23 32。

第三步:第二步的步长为10,故第三步的步长为10/1.3=7。故第0个元素3与第7个元素7进行比较,3小于7,不进行交换;同理,第1个元素7与第8个元素16进行比较,7小于9,不进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 7 1 6 9 11 4 7 16 31 22 10 18 22 41 25 31 23 32。

  第四步:第三步的步长为10,故第四步的步长为10/1.3=7。故第0个元素3与第7个元素7进行比较,3小于7,不进行交换;同理,第1个元素7与第8个元素16进行比较,7小于9,不进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 7 1 6 9 11 4 7 16 31 22 10 18 22 41 25 31 23 32。

  第五步;第四步的步长为7,故第五步的步长为7/1.3=5。故第0个元素3与第5个元素11进行比较,3小于11,不进行交换;同理,第1个元素7与第6个元素4进行比较,7大于4,进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 4 1 6 9 11 7 7 16 31 22 10 18 22 41 25 31 23 32。

  第六步;第五步的步长为5,故第六步的步长为5/1.3=3。故第0个元素3与第3个元素6进行比较,3小于6,不进行交换;同理,第1个元素4与第4个元素9进行比较,4大于9,不进行交换;以此类推,比较到最后一个元素后,此时数组变成:3 4 1 6 7 10 7 9 11 18 22 16 31 22 23 25 31 41 32。

  第七步;第六步的步长为3,故第七步的步长为3/1.3=2。故第0个元素3与第2个元素1进行比较,3大于1,进行交换;同理,第1个元素4与第3个元素6进行比较,4小于6,不进行交换;以此类推,比较到最后一个元素后,此时数组变成:1 4 3 6 7 9 7 10 11 16 22 18 23 22 31 25 31 41 32。   

第八步;第七步的步长为2,故第八步的步长为2/1.3=1。故第0个元素1与第1个元素4进行比较,1大于4,不进行交换;同理,第1个元素4与第2个元素3进行比较,4大于3,进行交换;以此类推,比较到最后一个元素后,此时数组变成:1 3 4 6 7 7 9 10 11 16 18 22 22 23 25 25 31 31 32 41。最后需要再经历一次冒泡,才算是完成排序,这个大佬给的例子是正好的,大家有时候自己找例子,他不是正好的。梳子排序是冒泡排序的改进。

#include <iostream>
#include<vector>
using namespace std;
void comb_sort(vector<int> &num){
   int step = num.size()/1.3;
   while(step >= 1){
   	for(int i = 0;i + step < num.size();i += step){
   		if(num[i] > num[i+step]) swap(num[i],num[i+step]);
   	}
   	step /= 1.3;
   }
   
}
void bubble_sort(vector<int>& num){
for(int i=0; i < num.size();i++)
   for(int j = 0;j < num.size() - i - 1;j++)
   	if(num[j] > num[j + 1]) {
   		int temp = num[j];
   		num[j] = num[j+1];
   		num[j+1] = temp;
   	}
}
int main() {
   vector<int> num = {2,5,3,5,0,1};
   comb_sort(num);
   bubble_sort(num);
   for(int i = 0; i < num.size();i++) cout<<num[i]<<endl;
   return 0;
}

参考(复制了不少,不过代码都是我自己写的,希望大家多少可以抄一下代码):

[1] 常用排序算法总结,力扣(LeetCode)

[2] 奇偶排序,半天妖

[3] 2023数据结构考研复习指导,王道论坛

[4] 基数排序,skywang12345

[5] 木桶排序,香格里拉太子zo

[6] 鸡尾酒排序,ouyangjun__

[7] Gnome排序(地精排序),ryan_jianjian

[8] 卷起来,浅析堆排序算法!,程序员小六

[9] 计算机经典算法——锦标赛排序算法,cure_py

[10] 梳排序(CombSort),Disappear_XueChao