C++中的排序算法
1. 排序的概念
排序是将一批无序的记录(数据)重新排列成按关键字有序的记录序列的过程。
2. 排序的稳定性
- 稳定排序:对于关键字相等的记录,排序前后相对位置不变。
- 不稳定排序:对于关键字相等的记录,排序前后相对位置可能发生变化。
3. 内部排序和外部排序
- 内部排序:在排序期间,需要将待排序元素整体添加到内存中排序
- 外部排序:在排序期间,不需要将待排序元素整体添加到内存中(归并是外部排序)
4. 排序的分类
- 比较类型排序
- 插入方式排序:插入排序、希尔排序
- 选择排序:直接选择排序、堆排序
- 交换排序:冒泡排序、快速排序
- 归并排序
- 非比较排序:计数排序和桶排序
5. 详述各个排序算法
5.1 插入排序
- 概念:
插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。
- 实现思路:
在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 。
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用场景:接近有序或者元素个数较少
//插入排序
void InsertSort(int array[], int size)
{
for (int i = 0; i < size; ++i)
{
//取待插入元素
int key = array[i];
//取待插入元素的前一个位置的下标
int end = i - 1;
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
array[end + 1] = key;
}
}
5.2 希尔排序
- 希尔排序的思想:(一般将增量设置为dk=dk/3+1,初始值为数组元素个数)
- 先取一个小于数组长度的增量d1作为第一个增量,把数组元素全部记录分组。所有距离为d1的倍数的放在同一个组中
- 然后开始循环对各组相邻增量元素,进行插入排序,只不过在这个插入排序中每次元素移动后,end的减少是以当前增量为单位的
- 当所有组内都已经排序好后,再将增量设置为更小的值继续执行上述步骤,直到增量变为1,实现最后一次插入排序,则排序完成
- 时间复杂度: O(n^(1.3~2))
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用场景:数据量较小且基本有序时
void ShellSort(int array[], int first, int last)
{
int dk = last - first;//初始增量
while (dk > 1)
{
dk = dk / 3 + 1;//控制增量逐步减小
for (int i = first ; i < last - dk; ++i)//每轮循环次数为总长度减去一个增量的距离
{
if (array[i + dk] < array[i])//比较相邻增量的两数
{
int end = i;//记录下标
int tmp = array[end + dk];//记录值
while (end >= first && tmp < array[end])//对每组进行插入排序
{
array[end + dk] = array[end];
end -= dk;
}
array[end + dk] = tmp;
}
}
}
}
5.3 直接选择排序
- 直接选择排序的思想:
从头到尾对序列进行扫描,每一轮通过比较找到序列中的最小值(最大值)放进数组中,然后后续的所有轮次中这样做,直到所有元素都排完为止,最终得到一个有序的数组
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用场景:当数据规模较小时,选择排序性能较好
void SelectionSort(int array[], int len) {
for (int i = 0; i < len-1; i++) {//外层只需循环len-1次,最后一个数不用再循环
int min = array[i]; //初始时的最小值
int index = i;//记录初始时的最小值下标
for (int j = i+1; j < len; j++) {//内层中每次从所有剩余元素的第二个位置开始比较
if (array[j] < min) {
min = array[j];//查找最小值
index = j;//将最小数的下标保存
}
}
//把该轮中的最小数放入数组中
int temp = array[index];
array[index] = array[i];
array[i] = temp;
}
}
5.4 堆排序
- 堆排序的概念:
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
- 堆排序的思想:
先将整个堆调整为一个大堆,调整完之后,每次将堆顶元素和堆的最后一个元素交换,之后end- -,将交换后的节点排除出去,然后再对堆进行调整,最终得到一个有序序列。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 适用场景:堆排序适合处理数据量大的情况,及数据呈流式输入的情况
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void _AdjustDown(int* elem, int first, int last, int start)//从每个分支自顶向下调整为大堆
{
int n = last - first;
//建立父节点指标和子节点指标
int i = start;
int j = 2 * i + 1;
int tmp = elem[i];//保存父节点值
while (j < n)//如果子节点指标在范围内才做比较
{
if (j + 1 < n && elem[j] < elem[j + 1])//先比较两个子节点,选择大的
j++;
if (tmp < elem[j])//如果父节点的值小于子节点,就交换父子内容,再继续子节点和孙节点比较
{
elem[i] = elem[j];
i = j;
j = 2 * i + 1;
}
else
break;
}
elem[i] = tmp;
}
void HeapSort(int* elem, int first, int last)
{
//调整成大堆
int n = last - first;
int curpos = n / 2 - 1;
while (curpos >= 0)
{
_AdjustDown(elem, first, last, curpos);
curpos--;
}
//排序
int end = last - 1;
while (end > first)//每次将堆顶元素和堆的最后一个元素交换,之后end--,将交换后的节点排除出去,然后再对堆进行调整,最终得到一个有序序列
{
Swap(&elem[end], &elem[first]);
end--;
_AdjustDown(elem, first, end + 1, first);
}
}
5.5 冒泡排序
- 冒泡排序思想:
- 嵌套两层for循环,外层负责控制排序的总趟数为len-1次,内层负责控制每躺循环的比较次数len-1-i
- (升序)在每次比较时如果j位置的元素>j+1位置的元素,则将其交换,否则不交换,继续下次比较
- 最终循环结束,排序完成
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
- 适用场景:当数据已经基本有序,且数据量较小时
void bubbleSort(int array[], int len)
{
int temp;
int i, j;
for (i = 0; i < len - 1; i++) // 外循环为排序趟数,len个数进行len-1趟
for (j = 0; j < len - 1 - i; j++)
{ //内循环为每趟比较的次数,第i趟比较len-i次
if (array[j] > array[j + 1])
{ //相邻元素比较,若逆序则交换(升序为左大于右,降序反之)
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
5.6 快速排序
- 实现方法:直接快速排序法、挖坑法、前后指针(主要针对划分方式上有差异)
- 实现思路:
- 取一个基准值(区间最右侧的数据或区间最左侧的数据)
- 按照基准值对区间中的元素进行划分:基准值左侧的数据小于基准值,基准值右侧的数据大于基准值,返回值基准值在划分好的区间中的下标
- [left, div) [div, right)继续递归使用快排的思想来进行排序
- 快速排序空间复杂度:O(log2^N)
递归算法:单次递归所需要耗费的空间(没有借助赋值空间是常量)*递归的深度
- 快速排序时间复杂度:
- 最差情况:如果每次分割时,机制准都是区间中的最大值|最小值,则分割完成:基准值一侧肯定是没有数据的
图解:最终就是一个单支树,而每次进行数据划分,都是将区间中的每个元素遍历了—遍,因此三个划分时间复杂度都是:O(N)- 最优情况:如果每次分割时,基准值都是区间中所有元素最中间的元素,则分割完成之后,基准值左右两侧的数据是均等的
图解:分割完成后是一个平衡二叉树,每一层的时间复杂度是O(N,深度是log2^N,则时间复杂度O(Nlog2 ^N)
- 稳定性:不稳定
- 适用场景:数据量稍微比较大,数据越随机越好
- 优化:针对取极值的方式进行优化,让取到极值的概率尽可能的降低 采用三数取中方式:最左侧、最右侧、中间,然后以三个数据最中间的数据作为基准值
- 存在问题:采用递归的方式来进行处理,当数据量非常大—>递归层次非常深—>可能就会导致栈溢出
- 解决方法:递归越深,每次递归期间,数据量越少,比较适合插入排序,让递归深度较深导致栈溢出的可能性降低
//三数取中找基准值
int GetMiddleIndex(int array[], int left, int right)
{
int mid = left + ((right - left) >> 1);
if (array[left] < array[right - 1])
{
if (array[mid] < array[left])
return left;
else if (array[mid] > array[right - 1])
return right - 1;
else
return mid;
}
else
{
// array[left] > array[right-1]
if (array[mid] > array[left])
return left;
else if (array[mid] < array[right - 1])
return right - 1;
else
return mid;
}
}
//直接快速排序
int partion(int array[],int left,int right)
{
int midIdx = GetMiddleIndex(array, left, right);
swap(array[midIdx], array[right - 1]);
int key = array[right - 1];
//下标
int begin = left;
int end = right - 1;
while (begin < end)
{
while (begin < end && array[begin] <= key)
{
begin++;
}
while (begin < end && array[end] >= key)
{
end--;
}
if(begin<end)
{
swap(array[begin], array[end]);
}
}
//如果key是区间的最大值
if (begin != right - 1)
swap(array[begin], array[right-1]);
return begin;
}
//挖坑法
int partion1(int array[], int left, int right)
{
int midIdx = GetMiddleIndex(array, left, right);
swap(array[midIdx], array[right - 1]);
int key = array[right - 1];
//下标
int begin = left;
int end = right - 1;
while (begin < end)
{
while (begin < end && array[begin] <= key)
begin++;
if (begin < end)
{
array[end] = array[begin];
end--;
}
while (begin < end && array[end] >= key)
end--;
if (begin < end)
{
array[begin] = array[end];
begin++;
}
}
array[begin] = key;
return begin;
}
//前后指针法
int partion2(int array[], int left, int right)
{
int midIdx = GetMiddleIndex(array, left, right);
swap(array[midIdx], array[right - 1]);
int key = array[right - 1];
//下标
int cur = left;
int prev = cur - 1;
while (cur < right)
{
if (array[cur] < key && ++prev != cur)
{
swap(array[cur], array[prev]);
}
++cur;
}
if (++prev != right - 1)
swap(array[prev], array[right - 1]);
return prev;
}
//快速排序
void QuickSort(int array[], int left, int right)
{
if (right - left > 1)
{
int div = partion2(array, left, right);
//递归排基准值的左侧
QuickSort(array, left, div);
//递归排基准值的右侧
QuickSort(array, div, right);
}
}
- 借助栈将递归转换成循环,实现循环快排
#include <stack>
void QuickSortNor(int array[], int size)
{
stack<int> s;
s.push(size);
s.push(0);
while (!s.empty())
{
int left = s.top();
s.pop();
int right = s.top();
s.pop();
if (right - left > 1)
{
int div = partion2(array, left, right);
// [div+1, right)
s.push(right);
s.push(div + 1);
// [left, div)
s.push(div);
s.push(left);
}
}
}
5.7 归并排序
- 实现思路:
- 先将数组中的元素先递归均分成最小的单个元素,然后对单个元素两两归并成有序的数组,然后在逐层向上归并,每次归并后的数组都是有序的,最后合成一个完整的有序数组
- 归并的方法是对每两个有序数组中从左到右的元素依次进行比较,每次将较小的元素拷贝到temp数组里,然后在对该数组中的元素位置加一,然后再进行比较,直到有一方数组中的元素走完,再将另一个数组的剩余元素也拷贝到temp中即可。
- 最终把temp里最后存放的值拷贝到array数组中,就完成了排序(注意要对临时数组开辟空间,使用完释放空间)
- 时间复杂度:O(Nlog2^N)
- 空间复杂度:O(N)
- 稳定性:稳定
- 适用场景:数据量较大且要求排序稳定时
思路图解:
- 最后一次合并的过程:
void MergeData(int array[], int left, int mid, int right, int* temp)
{
int begin1 = left, end1 = mid;
int begin2 = mid , end2 = right ;
int index = left;
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
temp[index++] = array[begin1++];
else
temp[index++] = array[begin2++];
}
while (begin1 < end1)
{
temp[index++] = array[begin1++];
}
while (begin2 < end2)
{
temp[index++] = array[begin2++];
}
}
void _MergeSort(int array[],int left,int right,int* temp)
{
if (right - left > 1)//至少有两个元素
{
int mid = left + ((right - left) >> 1);
//[left,mid)
_MergeSort(array, left, mid, temp);
//[mid,right)
_MergeSort(array, mid, right, temp);
// 需要将左半侧和右半侧的数据进行归并
MergeData(array, left, mid, right, temp);
// 将temp中的元素拷贝会array
memcpy(array + left, temp + left, sizeof(array[0]) * (right - left));
}
}
void MergeSort(int array[], int size)
{
int* temp = new int[size];
_MergeSort(array, 0, size, temp);
delete[]temp;
}