【数据结构】十大排序算法—— C++实现
😉🥰😚😙🤩😝😁😅🤭😄🙂
人类为何
都爱看天空
“ 这次东西太多,就先不学一个单词热热身了!”
Contents
-
0 算法概述 -
1 冒泡排序 -
2 选择排序 -
3 插入排序 -
4 希尔排序 -
5 归并排序 -
6 选择排序 -
7 堆排序(Heap Sort) -
8 计数排序 -
9 桶排序 -
10 基数排序
0 算法概述
0.1 算法分类
十种常见排序算法可以分为两大类:
(选泡插,快归堆希统计基)
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。它可以突破基于比较排序的时间下界,以线性时间O(n)运行,因此也称为线性时间非比较类排序。
关于比较和非比较排序:
常见的快速排序、归并排序、堆排序、冒泡排序 等属于比较排序 。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置 。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序 。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置 。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
0.2 算法复杂度
0.3 相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
01
—
这个算法名字的由来是因为越小的元素会经交换慢慢"浮"到数列的顶端。作为最简单的排序算法之一,冒泡排序给人的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
下一趟对前n-1个数做同样的操作。
持续每次对越来越少的元素重复以上步骤,直到没有任何一对数字需要比较。
/********************冒泡排序********************/
//相邻元素比较:选择大的元素放在后面
//非相邻元素比较,则是插入法的思想
/********************冒泡排序********************/
void BubbleSort(int a[], int len) {
bool exchange = true;
for (int i = len - 1; i > 0 && exchange; i--) {
int temp = 0;
exchange = false;
for (int j = 0; j < len - 1; j++) {
if (a[j] > a[j + 1]) {
temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
exchange = true;
}
}
}
}
02
—
包含n个元素的选择排序可经过n-1趟选择排序得到有序结果。
初始状态:无序区为R[1…n],有序区为空;
第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R[i]交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
经过n-1趟选择排序,数组有序化。
选择排序是表现最稳定的排序算法之一,因为对于任何数据排序的时间复杂度都是O(n2),故数据规模越大表现越差。唯一的好处是不占用额外的内存空间。
/********************选择排序********************/
//每次选择无序区中最小的元素与无序区中的首元素交换
//交换后有序区长度加一,无序区长度减一
/********************冒泡排序********************/
void SelectSort(int a[], int len) {
for (int i = 0; i < len; i++) {
for (int j = i+1; j < len ; j++) {
int temp = 0;
if (a[i] > a[j]) {
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
03
—
插入排序(Insertion-Sort)是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
从第一个元素开始,该元素可以认为已经被排序;
取出下一个元素的key,在已经排序的元素序列中从后向前扫描;
如果扫描到的元素(已排序)大于新元素key,将扫描到的元素移到下一位置;
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
将新元素插入到该位置后;
从接下来的n-1个元素开始重复步骤2~5。
void Insertsort(int a[], int len) {
for (int i = 1; i < len; i++) {
int temp = a[i];
int pos = i;
for (int j = i - 1; j >= 0 && a[j] > temp; j--) {
a[j + 1] = a[j];
pos = j;
}
a[pos] = temp;
}
}
04
—
1959年Shell发明第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。具体算法描述如下:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,这样可以让一个元素可以一次性地朝最终位置前进一大步,然后算法再取越来越小的步长进行排序。仅增量因子为1时,整个序列作为一个记录序列来处理,即最后一步进行普通的插入排序后,序列有序。
void ShellSort(int a[], int len) {
const int sublen = 2;
int InsertNum = 0;
unsigned gap = len / sublen;
while(gap) {
for (int i = gap; i < len; i++) {
InsertNum = a[i];
int j = i;
while (j >= gap && a[j - gap] > InsertNum) {
a[i] = a[j - gap];
j -= gap;
}
a[j] = InsertNum;
}
gap = gap / sublen;
}
}
05
—
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。具体算法描述如下:
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列。
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
void Merge(int a[], int low, int mid, int high) {
int n = high - low + 1;//临时数组存合并后的有序序列
int* tmp = new int[n];
int i = 0;
int left = low;
int right = mid + 1;
while (left <= mid && right <= high)
tmp[i++] = a[left] <= a[right] ? a[left++] : a[right++];
while (left <= mid)
tmp[i++] = a[left++];
while (right <= high)
tmp[i++] = a[right++];
//将融合后的数据拷贝到原来的数据对应的子空间中
i = 0;
while(low <= high)
a[low++] = tmp[i++];
delete[] tmp;//删掉堆区的内存
}
void MergeSort(int a[], int low, int high) {
if (low == high)
return; //递归基是让数组中的每个数单独成为长度为1的区间
int mid = (low + high) / 2;
MergeSort(a, low, mid);
MergeSort(a, mid+1, high);
Merge(a, low, mid, high);
}
06
—
具体算法描述如下:
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
根据分治、递归的处理思想,可以用递归排序下标从 p 到 q-1 之间的数据和下标从 q+1 到 r 之间的数据,直到区间缩小为 1,就说明所有的数据都有序了。这里涉及到基准的选择问题,因此必须有函数Partition()来实现“基准”,该函数应返回基准的索引。
int partition(int a[], int low, int high) {
int pivot = a[high];
int i = low;
for (int j = i; j < high; j++) {
if (a[j] < pivot) {
swap(a[j], a[i]);
i++;
}
}
swap(a[high], a[i]);
return i;
}
void QuickSort(int a[], int low, int high) {
if (low < high) {
int q = partition(a, low, high);
QuickSort(a, low, q - 1);
QuickSort(a, q + 1, high);
}
}
07
—
堆是完全二叉树的结构,因此对于一个有n个节点的堆,高度为O(logn)。
最大堆:堆中的最大元素存放在根节点的位置。除了根节点,其他每个节点的值最多与其父节点的值一样大。也就是任意一个子树中包含的所有节点的值都不大于其树根节点的值。
堆中节点的位置编号都是确定的,根节点编号为1,每一层从左到右依次编号。由堆是完全二叉树,可以知道当堆中某个节点的编号为i时,如果这个节点有左右子树,那么左子树的节点编号为2×i,右子树的节点编号为2×i+1(在根节点编号为1的情况时)。
堆排序算法:
初始时,算法利用buildMaxHeap将数组A[1…n]建成最大堆,因为数组中最大元素总在根节点A[1]处,通过把它与A[n]进行互换,可以让该元素放到正确位置。之后从堆中去掉结点n,调用maxHeap(A, 1),从而在A[1…n-1]上构造一个新的最大堆。之后不断重复这样的操作,直到堆的大小从n-1降到2。
/********************堆排序********************/
// 最大堆 树根节点元素最大,每次将树根节点元素取出 即取到了序列最大元素
// 将最大元素去掉后的剩余序列构成最大堆,不断取出最大堆的树根节点
// 直到最大堆最后剩两个节点,排序完毕
// 堆是完全二叉树,从下向上构建最大堆
/**********************End***********************/
void heapSort(int a[], int n, int len);
void maxHeap(int a[], int n, int len);
void buildMaxHeap(int a[], int n, int len);
void heapSort(int a[], int n, int len)
{
buildMaxHeap(a, n, len);
for (int i = len; i > 1; i--)
{
swap(a[0], a[i - 1]);
len = len - 1;
maxHeap(a, 1, len);
}
}
void buildMaxHeap(int a[], int n, int len)
{
int i = n / 2;
for (i; i > 0; i--)
maxHeap(a, i, len);
}
void maxHeap(int a[], int n, int len)
{
int leftChild, rightChild, largest;
leftChild = 2 * n;
rightChild = 2 * n + 1;
if (leftChild <= len && a[leftChild - 1] > a[n - 1])
largest = leftChild;
else
largest = n;
if (rightChild <= len && a[rightChild - 1] > a[largest - 1])
largest = rightChild;
if (largest != n)
{
swap(a[n - 1], a[largest - 1]);
maxHeap(a, largest, len);
}
}
08
—
计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序O(n),计数排序要求输入的数据必须是有确定范围的整数。(整个过程可概括为先进行直方图统计,再按照顺序扔出来)。具体算法描述如下:
找出待排序的数组中最大和最小的元素;
统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
/********************计数排序********************/
// 需要得到序列中的数据分布范围,即序列的最大值和最小值
// 要求待排序序列必须是非负整数
// 在新开辟空间中记录原始序列中每个值出现的次数
// 之后再根据新开辟空间中对应索引存储值的个数来返回有序序列
/**********************End***********************/
void CountSort(int a[], int len) {
int temp[100] = { 0 };
int n = 0;
int sortIndex = 0;
for (int i = 0; i < len; i++) {
temp[a[i]]++;
}
for (int i = 0; i < 100; i++) {
while (temp[i] > 0) {
a[sortIndex++] = i;
temp[i]--;
}
}
}
09
—
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理是将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。具体算法描述如下:
设置一个定量的数组当作空桶;
遍历输入数据,并且把数据一个一个放到对应的桶里去;
对每个不是空的桶进行排序;
从不是空的桶里把排好序的数据拼接起来。
/********************桶排序**********************/
// 首先得到待排序序列的最大最小值
// 根据给定桶的容量 来确定桶的个数
// 将待排序序列分别放入不同的桶中
// 对于不同的桶 利用以上任意一种排序算法排序
// 将桶中的数据按照桶的顺序进行整体连接排序
// 本次选用计数排序对 桶中的序列排序
/**********************End***********************/
void CountSort_vct(vector<int>& a) {
int temp[100] = { 0 };
int n = 0;
int sortIndex = 0;
for (int i = 0; i < a.size(); i++) {
temp[a[i]]++;
}
for (int i = 0; i < 100; i++) {
while (temp[i] > 0) {
a[sortIndex++] = i;
temp[i]--;
}
}
}
void bucketSort(vector<int>& arr, int bucketSize = 10) {
if (arr.size() == 0) {
return;
}
int i;
int minValue = arr[0];
int maxValue = arr[0];
for (i = 1; i < arr.size(); i++) {
if (arr[i] < minValue) {
minValue = arr[i]; // 待排序序列最小值
}
else if (arr[i] > maxValue) {
maxValue = arr[i]; // 待排序序列最大值
}
}
int bucketCount = int((maxValue - minValue) / bucketSize) + 1; //根据最大最小值计算桶的个数
vector<vector<int>> buckets(bucketCount, vector<int>());
// 利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.size(); i++) {
int idx = int((arr[i] - minValue) / bucketSize);
buckets[idx].push_back(arr[i]);
}
arr.clear();
for (i = 0; i < buckets.size(); i++) {
CountSort_vct(buckets[i]); // 对每个桶进行排序,这里使用了计数排序,可以用上面的任意一种排序代替
for (int j = 0; j < buckets[i].size(); j++) {
arr.push_back(buckets[i][j]);
}
}
return;
}
10
—
-
取得数组中的最大数,并取得其位数; -
arr为原始数组,从最低位开始取每个位组成radix数组; -
对radix进行计数排序(利用计数排序适用于小范围数的特点);
和计数排序一样,基数排序仅仅可以对非负整数进行排序,其时间复杂度为O(n),且计数排序、桶排序、基数排序均为稳定排序算法。
/********************基数排序**********************/
// 获得待排序序列中最大数的 位数
// 分别对待排序序列的个位、十位、百位……进行桶排序
// 最后获得有序序列 【仅可对非负整数排序】 稳定算法
/**********************End***********************/
int maxbit(int data[], int n)
{
int d = 1; //保存最大的位数
int p = 10;
for (int i = 0; i < n; ++i)
{
while (data[i] >= p)
{
p *= 10;
++d;
}
}
return d;
}
void radixsort(int data[], int n) //基数排序
{
int d = maxbit(data, n);
int tmp[100];
int count[10]; //计数器
int i, j, k;
int radix = 1;
for (i = 1; i <= d; i++) //进行d次排序
{
for (j = 0; j < 10; j++)
count[j] = 0; //每次分配前清空计数器
for (j = 0; j < n; j++)
{
k = (data[j] / radix) % 10; //统计每个桶中的记录数
count[k]++;
}
for (j = 1; j < 10; j++)
count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
{
k = (data[j] / radix) % 10;
tmp[count[k] - 1] = data[j];
count[k]--;
}
for (j = 0; j < n; j++) //将临时数组的内容复制到data中
data[j] = tmp[j];
radix = radix * 10;
}
}
🔗往期分享