目录
前言
1、选择排序
介绍
参考代码
2、冒泡排序
介绍
参考代码
3、插入排序
介绍
参考代码
4、希尔排序
介绍
参考代码
5、快速排序
介绍
参考代码
6、并归排序
介绍
参考代码
7、堆排序
介绍
参考代码
8、基数排序
介绍
参考代码
9、计数排序
介绍
参考代码
10、桶排序
介绍
参考代码
总结
前言
本期我们将学习C++常见的十种排序方式,它们的优缺点作者都会写在这里。
排序(Sorting)是计算机程序设计中的一种重要操作,其功能是对一个数据元素集合或序列重新排列成一个按数据元素某个项值有序的序列。
常见的快速排序、归并排序、堆排序以及冒泡排序等都属于比较类排序算法。比较类排序是通过比较来决定元素间的相对次序,由于其时间复杂度不能突破,因此也称为非线性时间比较类排序。在冒泡排序之类的排序中,问题规模为 。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为 次,所以时间复杂度平均 。
比较类排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
而计数排序、基数排序、桶排序则属于非比较类排序算法。非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。由于它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度.
非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
1、选择排序
介绍
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。
每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。
时间复杂度:最坏情况:
最好情况:
空间复杂度:
参考代码
//选择排序
void swap(int* a, int* b)
{
int tem = *a;
*a = *b;
*b = tem;
}
void SelectSort(int* arr, int n)
{
//保存参与单趟排序的第一个数和最后一个数的下标
int begin = 0, end = n - 1;
while (begin < end)
{
//保存最大值的下标
int maxi = begin;
//保存最小值的下标
int mini = begin;
//找出最大值和最小值的下标
for (int i = begin; i <= end; ++i)
{
if (arr[i] < arr[mini])
{
mini = i;
}
if (arr[i] > arr[maxi])
{
maxi = i;
}
}
//最小值放在序列开头
swap(&arr[mini], &arr[begin]);
//防止最大的数在begin位置被换走
if (begin == maxi)
{
maxi = mini;
}
//最大值放在序列结尾
swap(&arr[maxi], &arr[end]);
++begin;
--end;
}
}
2、冒泡排序
介绍
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
左边大于右边交换一趟排下来最大的在右边。
时间复杂度:最坏情况:
最好情况:
空间复杂度:
参考代码
//冒泡排序
void BubbleSort(int* arr, int n)
{
int end = n;
while (end)
{
int flag = 0;
for (int i = 1; i < end; ++i)
{
if (arr[i - 1] > arr[i])
{
int tem = arr[i];
arr[i] = arr[i - 1];
arr[i - 1] = tem;
flag = 1;
}
}
if (flag == 0)
{
break;
}
--end;
}
}
3、插入排序
介绍
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 。
在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。
但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。
时间复杂度:最坏情况下为O(N*N),此时待排序列为逆序,或者说接近逆序
最好情况下为O(N),此时待排序列为升序,或者说接近升序。
空间复杂度:O(1)
参考代码
void InsertSort(int* arr, int n)
{
for (int i = 0; i < n - 1; ++i)
{
//记录有序序列最后一个元素的下标
int end = i;
//待插入的元素
int tem = arr[end + 1];
//单趟排
while (end >= 0)
{
//比插入的数大就向后移
if (tem < arr[end])
{
arr[end + 1] = arr[end];
end--;
}
//比插入的数小,跳出循环
else
{
break;
}
}
//tem放到比插入的数小的数的后面
arr[end + 1] = tem;
//代码执行到此位置有两种情况:
//1.待插入元素找到应插入位置(break跳出循环到此)
//2.待插入元素比当前有序序列中的所有元素都小(while循环结束后到此)
}
}
4、希尔排序
介绍
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
希尔排序,先将待排序列进行预排序,使待排序列接近有序,然后再对该序列进行一次插入排序,此时插入排序的时间复杂度为O(N)。
时间复杂度平均:
空间复杂度:
参考代码
//希尔排序
void ShellSort(int* arr, int n)
{
int gap = n;
while (gap>1)
{
//每次对gap折半操作
gap = gap / 2;
//单趟排序
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tem = arr[end + gap];
while (end >= 0)
{
if (tem < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
else
{
break;
}
}
arr[end + gap] = tem;
}
}
}
5、快速排序
介绍
快速排序(Quicksort),计算机科学词汇,适用领域Pascal,C++等语言,是对冒泡排序算法的一种改进。
1、选出一个key,一般是最左边或是最右边的。
2、定义一个begin和一个end,begin从左向右走,end从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要end先走;若选择最右边的数据作为key,则需要bengin先走)。
3、在走的过程中,若end遇到小于key的数,则停下,begin开始走,直到begin遇到一个大于key的数时,将begin和right的内容交换,end再次开始走,如此进行下去,直到begin和end最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序。
时间复杂度:
快速排序的过程类似于二叉树其高度为logN,每层约有N个数,如下图所示:
参考代码
//快速排序 hoare版本(左右指针法)
void QuickSort(int* arr, int begin, int end)
{
//只有一个数或区间不存在
if (begin >= end)
return;
int left = begin;
int right = end;
//选左边为key
int keyi = begin;
while (begin < end)
{
//右边选小 等号防止和key值相等 防止顺序begin和end越界
while (arr[end] >= arr[keyi] && begin < end)
{
--end;
}
//左边选大
while (arr[begin] <= arr[keyi] && begin < end)
{
++begin;
}
//小的换到右边,大的换到左边
swap(&arr[begin], &arr[end]);
}
swap(&arr[keyi], &arr[end]);
keyi = end;
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(arr, left, keyi - 1);
QuickSort(arr,keyi + 1,right);
}
//快速排序法 挖坑法
void QuickSort1(int* arr, int begin, int end)
{
if (begin >= end)
return;
int left = begin,right = end;
int key = arr[begin];
while (begin < end)
{
//找小
while (arr[end] >= key && begin < end)
{
--end;
}
//小的放到左边的坑里
arr[begin] = arr[end];
//找大
while (arr[begin] <= key && begin < end)
{
++begin;
}
//大的放到右边的坑里
arr[end] = arr[begin];
}
arr[begin] = key;
int keyi = begin;
//[left,keyi-1]keyi[keyi+1,right]
QuickSort1(arr, left, keyi - 1);
QuickSort1(arr, keyi + 1, right);
}
//单趟排
int PartSort(int* arr, int begin, int end)
{
int key = arr[begin];
while (begin < end)
{
while (key <= arr[end] && begin < end)
{
--end;
}
arr[begin] = arr[end];
while (key >= arr[begin] && begin < end)
{
++begin;
}
arr[end] = arr[begin];
}
arr[begin] = key;
int meeti = begin;
return meeti;
}
void QuickSortNoR(int* arr, int begin, int end)
{
stack<int> st;
//先入右边
st.push(end);
//再入左边
st.push(begin);
while (!st.empty())
{
//左区间
int left = st.top();
st.pop();
//右区间
int right = st.top();
st.pop();
//中间数
int mid = PartSort(arr, left, right);
//当左区间>=mid-1则证明左区间已经排好序了
if (left < mid - 1)
{
st.push(mid - 1);
st.push(left);
}
//当mid+1>=右区间则证明右区间已经排好序
if (right > mid + 1)
{
st.push(right);
st.push(mid + 1);
}
}
}
//快速排序法 前后指针版本
void QuickSort2(int* arr, int begin, int end)
{
if (begin >= end)
return;
int cur = begin, prev = begin - 1;
int keyi = end;
while (cur != keyi)
{
if (arr[cur] < arr[keyi] && ++prev != cur)
{
swap(&arr[cur], &arr[prev]);
}
++cur;
}
swap(&arr[++prev],&arr[keyi]);
keyi = prev;
//[begin,keyi -1]keyi[keyi+1,end]
QuickSort2(arr, begin, keyi - 1);
QuickSort2(arr, keyi + 1, end);
}
6、并归排序
介绍
归并排序(Merge Sort)是一种经典的排序算法,它采用了分治法的策略。
将初始序列的n个元素看成n个有序的子序列,每个子序列中只有一个元素,将其两两归并,得到n/2个长度为2(或1、子序列不为偶数则有落单)的有序子序列,再两两归并…以此类推直到得到n长的有序序列。
时间复杂度
空间复杂度
参考代码
/*
题目:归并排序
划分成很小的组,然后两两归并
*/
#include<iostream>
using namespace std;
void Merge(int[], int, int[], int, int, int) //归并函数的声明【把归并函数提到该函数前面,则不用声明】
//归并排序
//参数:
// numbers[]:原数组
// length:数组元素的个数(数组长度)
// temp[]:辅助数组
// begin:数组开头的下标
// end:数组结尾的下标
void MergeSort(int numbers[], int length, int temp[], int begin, int end)
{
//1. 同样判断传入的参数是否有效
if (numbers == nullptr || length <= 0 || begin < 0 || end >= length)
throw new exception("Invalid input.");
//2. 作为递归的结束条件,开始下标和结束下标相等时,说明子序列中只有一个元素,看作有序的
if (end - begin == 0)
return;
//3. 定义中间变量,将数组分半【如果有7个元素,下标0-6,则middle=3,数组分为长度为4和3的两段】
int middle = ((end - begin) / 2 ) + begin;
//4. 递归,先递归左半边,再递归右半边,将左右子序列不断分为长度为1的子序列才停止递归
MergeSort(numbers, length, temp, begin, middle);
MergeSort(numbers, length, temp, middle + 1, end);
//5. 再慢慢归并
Merge(numbers, length, temp, begin, end, middle);
}
//归并函数
//参数:
// numbers[]:原数组
// length:数组元素的个数(数组长度)
// temp[]:辅助数组
// begin:数组开头的下标
// end:数组结尾的下标
// middle:数组中间的下标
void Merge(int numbers[], int length, int temp[], int begin, int end, int middle)
{
//1. 判断是否有不符合要求的参数传入,有则抛出错误
if (numbers == nullptr || length <= 0 || begin < 0 || end >= length)
throw new exception("Invalid input.");
//2. 将原序列从中分开
int leftIndex = begin; //左边序列的开始(左边序列的结尾是middle)
int rightIndex = middle + 1;//右边序列的开始(右边序列的结尾是end)
int tempIndex = begin; //辅助数组的下标
//3. 当左右子序列尚未到头时,循环
while (leftIndex <= middle && rightIndex <= end)
{
//4. 两两对比判断,谁大谁就放入辅助数组,同时指针后移
if (numbers[leftIndex] < numbers[rightIndex])
temp[tempIndex] = numbers[leftIndex++];
else
temp[tempIndex] = numbers[rightIndex++];
//5. 辅助数组下标++
++tempIndex;
}
//6. 当左边或右边子序列尚未到头时,直接放入辅助数组
while (leftIndex <= middle)
temp[tempIndex++] = numbers[leftIndex++];
while (rightIndex <= end)
temp[tempIndex++] = numbers[rightIndex++];
//7. 再将辅助数组中已经有序的元素覆盖掉原数组中无序的元素,使原数组变成部分有序
for (int i = begin; i <= end; ++i)
numbers[i] = temp[i];
}
//简单测试
int main(int arvc, char* argv[])
{
const int length = 9;
int nums[length] = { 18, 7, 23, 3, 9, 32, 10 , 99, 0};
int *temp = new int[length];
MergeSort(nums, length, temp, 0, 8);
for (int i = 0; i < length; i++)
cout << nums[i] << " ";
delete[] temp;
temp = nullptr;
cout << endl;
return 0;
}
7、堆排序
介绍
堆排序(英语:Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
参考代码
#include<iostream>
using namespace std;
void sw(int &a,int &b) {
int temp = a;
a = b;
b = temp;
}
// arr[]为完全二叉树层序遍历得到的数组
// n为完全二叉树的节点,即数组长度
// i为待维护的节点
void heapify(int arr[], int n, int i) { //把这个二叉树先堆化
//递归出口
if (i >= n) return;
int largest = i;
int lson = i * 2 + 1;
int rson = i * 2 + 2;
if (lson < n && arr[largest] < arr[lson]) { //和左孩子数值比较,找到最大节点,赋值下标
largest = lson;
}
if (rson < n && arr[largest] < arr[rson]) { //和右孩子数值比较,找到最大节点,赋值下标
largest = rson;
}
if (largest != i) { //如果现在的最大值下标和之前的不一样,那么交换二者的数值
//sw(arr[largest], arr[i]);
swap(arr[largest], arr[i]);
heapify(arr, n, largest); //进行一个递归,因为在上一层的节点交换完之后,无法保证下边父节点大于孩子节点
}
}
void heap_sort(int arr[], int n) {
//建堆
int lastNode = n - 1; //从后往前建堆
int parent = (lastNode - 1) / 2;
for (int i = parent; i >= 0; i--) {
heapify(arr, n, i);
}
//堆排序
for (int i = n - 1; i >= 0; i--) {
sw(arr[i], arr[0]);
heapify(arr, i, 0);
}
}
int main() {
int arr[5] = { 5,4,3,2,1 };
heap_sort(arr, sizeof(arr) / sizeof(arr[0]));
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
cout << arr[i] << endl;
}
return 0;
}
8、基数排序
介绍
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog(r)m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
1.计算出得到的序列中的位数最大的数字的位数,以便确定需要进行几次排序
2.创建取出任意数字任意位数的函数,用于对数字的排序
3.接下来便可以开始排序
时间复杂度:
空间复杂度:
参考代码
#include <iostream>
#include <vector>
using namespace std;
int MaxBit(vector<int> input) //求出数组中最大数的位数
{
int max_num = input[0]; //默认最大数为第一个数字
for (int i = 0; i < input.size(); i++) //找出数组中的最大数
{
if (input[i] > max_num)
{
max_num = input[i];
}
}
int p = 0;
while (max_num > 0)
{
p++;
max_num /= 10; //每次除以10取整,即可去掉最低位
}
return p;
}
int GetNum(int num, int d) //取出所给数字的第d位数字
{
int p = 1;
while (d - 1 > 0)
{
p *= 10;
d--;
}
return num / p % 10;
}
vector<int> RadixSort(vector<int> input, int length) //基数排序
{
vector<int> bucket(length); //创建临时存放排序过程中的数据
vector<int> count(10); //创建按位计数的技术容器,即记录排序中按个位、十位...各个数的位置的个数
for (int d = 1; d <= MaxBit(input); d++) {
// 计数器清0
for (int i = 0; i < 10; i++) {
count[i] = 0;
}
// 统计各个桶中的个数
for (int i = 0; i < length; i++) {
count[GetNum(input[i], d)]++;
}
for (int i = 1; i < 10; i++) { //得到每个数应该放入bucket中的位置
count[i] += count[i - 1];
}
for (int i = length - 1; i >= 0; i--) { //采用倒序进行排序是为了不打乱已经排好的顺序
int k = GetNum(input[i], d);
bucket[count[k] - 1] = input[i];
count[k]--;
}
for (int j = 0; j < length; j++) // 临时数组复制到 input 中
{
input[j] = bucket[j];
}
}
return input;
}
int main()
{
int arr[] = { 50, 123, 543, 187, 49, 30, 0, 2, 11, 100 };
vector<int> test(arr, arr + sizeof(arr) / sizeof(arr[0]));
cout << "排序前:";
for (int i = 0; i < test.size(); i++) {
cout << test[i] << " ";
}
cout << endl;
vector<int> result = test;
result = RadixSort(result, result.size());
cout << "排序后:";
for (int i = 0; i < result.size(); i++) {
cout << result[i] << " ";
}
cout << endl;
system("pause");
}
9、计数排序
介绍
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 [1] 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为
i
的元素出现的次数,存入数组C
的第i
项 - 对所有的计数累加(从
C
中的第一个元素开始,每一项和前一项相加) - 反向填充目标数组:将每个元素
i
放在新数组的第C[i]
项,每放一个元素就将C[i]
减去1
时间复杂度:
参考代码
#include <stdlib.h>
void countingSort(int *ini_arr, int *sorted_arr, int n, int maxValue)
{
int *count_arr = (int *)malloc(sizeof(int) * maxValue);
int i, j, k;
for (k = 0; k <= maxValue; k++)
count_arr[k] = 0;
for (i = 0; i < n; i++)
count_arr[ini_arr[i]]++;
for (k = 1; k <= maxValue; k++)
count_arr[k] += count_arr[k - 1];
for (j = n; j > 0; j--)
sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];
free(count_arr);
}
10、桶排序
介绍
桶排序(Bucket Sort)是一种排序算法,其工作原理是将数组分配到有限数量的桶中,然后对每个桶中的元素进行排序,最后再将各个桶中的数据有序的合并起来。
原理:将数值作为桶号,遍历整个数组,将相应的桶进行计数
1、 遍历原数组,找到最大值max,然后申请max+1个空间(桶),初始化为0(下标为0-max),即 vector<int>bucket(max+1,0)
2、 再次遍历原数组,找到每个数值对应的桶号,并对桶计数++,即bucket[vec[i]]++
3、 遍历桶数组,看对应的桶内计数为几就取出几下下标值(桶号),放到原数组中。
时间复杂度:
空间复杂度:
参考代码
#include<iostream>
#include<vector>
using namespace std;
void bucketSort(vector<int>&vec, int n)
{
int max = vec[0];
for (int i = 0; i < n; i++)
{
if (vec[i] > max)
{
max = vec[i];
}
}
//申请max+1个桶
//int *bucket = new int[max + 1];
//给每个桶赋初值为0;
//memset(bucket, 0, (max + 1) * sizeof(int));
vector<int>bucket(max + 1, 0);
//遍历原数组,把相应的数放到相应的桶里
for (int i = 0; i < n; i++)
{
bucket[vec[i]]++;
}
int index = 0;
//从桶里把数取出来, i代表的数值对应桶下标, bucket[i]代表的是个数
for (int i = 0; i < bucket.size(); i++)
{
while (bucket[i] > 0)
{
vec[index++] = i;
bucket[i]--;
}
}
}
int main()
{
vector<int>vec = { 2,3,5,8,9,7,4,6,1 };
bucketSort(vec, vec.size());
for (auto it : vec)
{
cout << it << " ";
}
return 0;
}
总结
在编写合理的情况下,简单排序算法是稳定的;快速排序、堆排序是不稳定的。在CSP中,往往排序是没有附带其他项目的,也就不要求排序稳定。快速排序、堆排序仍然是最佳选择。可是有没有时间复杂度为O(nlogn)的稳定的排序算法呢?有的。归并排序基于分治思想:把要排序的数组平分两半,对两部分分别排序(递归地)后再合并起来。合并时,将一个数组按顺序插入另一个数组中,需要开辟一个暂存数组。利用空间优化,可只用开辟一个与原数组等大的数组。归并排序的优缺点都很明显。无论情形如何,它的比较次数、赋值次数都稳定在nlogn,没有最差情况,运行时间与快速排序、堆排序相当。而且,它是稳定的排序算法。但是,它的内存占用会达到快速排序、堆排序的两倍,合理选用排序算法。
下面是一些主排序算法的优缺点比较:竞赛时使用容易造成内存超出限制。