目录
- [1 排序相关概念](#1 排序相关概念)
- [2 内部排序](#2 内部排序)
-
- [2.1 直接插入排序](#2.1 直接插入排序)
- [2.2 希尔排序](#2.2 希尔排序)
- [2.3 简单选择排序](#2.3 简单选择排序)
- [2.4 堆排序](#2.4 堆排序)
- [2.5 冒泡排序](#2.5 冒泡排序)
- [2.6 快速排序](#2.6 快速排序)
- [2.7 归并排序](#2.7 归并排序)
- [2.8 计数排序](#2.8 计数排序)
- [3 排序的总结](#3 排序的总结)
1 排序相关概念
- 排序 :对数据进行排序,就是将数据以某个值为标准,按照升序或降序的方式重新组织起来。比如,将 10 6 7 1 3 9 4 2 这一组数据进行升序排序,就会得到 1 2 3 4 6 7 9 10 的结果
- 稳定性 :排序前和排序后,数据的相对位置没有变化,那么该排序就是稳定的,否则是不稳定的。比如,数据 6 6 1 5 2 4 排序完后为 1 2 4 5 6 6 ,两个 6 的相对位置没有发生变化,该排序是稳定的
- 内部排序:排序时,全部数据都位于内存中
- 外部排序:排序时,内存中无法存放所有外存中的数据,需要在内存,外存之间来回移动元素的一种排序
2 内部排序
内部排序主要包括了插入排序,选择排序,交换排序,归并排序,计数排序
插入排序 包括了 直接插入排序 和希尔排序
选择排序 包括了简单选择排序 和堆排序
交换排序 包括了冒泡排序 和快速排序
2.1 直接插入排序
直接插入排序的思想是:
取出数组中的值 nums[i],将 nums[i] 与其前面的值(nums[0],nums[1]...nums[i-1])进行比较,遇到比 nums[i] 要大或要小的值,将它向后进行移动,当遇到值不再大于或小于 nums[i] 的值,将 nums[i] 插入至它的后方,直到数组有序为止
时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:稳定
c
//直接插入排序
void InsertSort(int* a,int n)
{
for (int i = 0;i < n - 1;++i)
{
int end = i;
int tmp = a[end + 1]; //保存数据,防止覆盖
while (end >= 0)
{
if (a[end] > tmp) //比较tmp(原end+1)与end指向的元素,若前面更大,则将前面的元素后移
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp; //当前面元素更小,插入至后方
}
}
2.2 希尔排序
希尔排序是基于直接插入排序来实现的,它的思想是:
设定一个数 gap 来对数据进行分组,对这些组内的数据使用直接插入排序,排完所有组内的数据后,使 gap 进行缩小,继续进行以上的操作,当最终 gap 为 1 时,就可以使得数据有序
希尔排序的时间复杂度测量起来十分复杂,所以在此给出大致的时间复杂度
时间复杂度:O(N1.3)
空间复杂度:O(1)
稳定性:不稳定
c
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 2;
//并行地对一组进行直接插入排序
for (int i = 0;i < n - gap;++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
2.3 简单选择排序
一般来说,简单选择排序的思想是:
每次选出剩下数据当中最大或最小的,将其放在当前区间的开头,直至最后有序
在这里给出简单选择排序的优化版本,它的基本思想是:
每次选出当前区间[begin, end]内最大和最小的数据,将最小的数据与当前区间最前面的值进行交换,将最大的数据与当前区间最后方的值交换,交换完成后,缩小区间并重复上述操作,直到最后有序
时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:不稳定
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//选择排序
void SelectSort(int* a, int n)
{
//区间[begin, end]
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1;i <= end;++i)
{
if (a[i] < a[mini]) //寻找最小值的下标
{
mini = i;
}
if (a[i] > a[maxi]) //寻找最大值的下标
{
maxi = i;
}
}
if (begin == maxi) //检查最大值是否与要替换的值一致
maxi = mini;
Swap(&a[begin], &a[mini]);
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
2.4 堆排序
堆排序需要使用到一种数据结构:堆 ,它本质上是顺序存储的完全二叉树
堆分为小堆和大堆
小堆:每个子树的根都小于它的左右孩子
大堆:每个子树的根都大于它的左右孩子
堆排序的思想是:
将待排序的数据集合看成是一个堆,通过对其不停地进行建堆,调整来使得堆顶值为当前区间内最小或最大的值,将该值与当前区间的最后一个值进行交换,就可以使该值位于最终位置上,重复以上操作直到最后数据有序
时间复杂度:O(NlogN)
空间复杂度:O(N)
稳定性:不稳定
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//向下调整
void AdjustDown(int* a, int n, int root)
{
while (root * 2 + 1 < n)
{
int child = 2 * root + 1; //左孩子
if (child + 1 < n && a[child] > a[child + 1]) //比较左右孩子,取小的
{
child = child + 1;
}
if (a[child] < a[root]) //孩子比父亲小,父亲下调
{
Swap(&a[child], &a[root]);
root = child;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
//调整为大堆
for (int i = (n - 2) / 2;i >= 0;--i)
{
AdjustDown(a, n, i);
}
//排序
for (int i = 1;i < n;++i)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
}
}
2.5 冒泡排序
冒泡排序的思想是:
通过比较前后两个值来获得它们的大小关系,若顺序不符合要求,则将两个值交换,当一趟排序结束时,就会有一个值位于最终位置上,对剩下的值进行同样的操作直到有序
时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:稳定
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int* nums, int n)
{
for (int i = 0;i < n;++i)
{
int flag = 0; //假设已经有序
for (int j = 0;j < n - i - 1;++j)
{
if (nums[j] > nums[j + 1])
{
Swap(&nums[j], &nums[j + 1]);
flag = 1; //进行了交换,不是有序
}
}
if (flag == 0)
break;
}
}
2.6 快速排序
快速排序的思想是:
选定一个值作为 key,不断地将其它值与 key 作比较,将比 key 小的值留在它的左侧,将比 key 大的值留在它的右侧,以此来划分左右区间,对左右区间进行同样的操作,直到最后有序
由于快速排序是递归实现的,所以排序的操作雷同,在这里只给出第一趟划分
时间复杂度:O(NlogN)
空间复杂度:O(logN)
稳定性:不稳定
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//快速排序
void QuickSort(int* nums, int begin, int end)
{
//只有一个元素,或者没有元素时
if (begin >= end)
{
return;
}
int keyi = begin;
int left = begin;
int right = end;
while (left < right)
{
//找小
while (left < right && nums[right] >= nums[keyi])
{
right--;
}
//找大
while (left < right && nums[left] <= nums[keyi])
{
left++;
}
Swap(&nums[left], &nums[right]);
}
Swap(&nums[left], &nums[keyi]);
keyi = left;
QuickSort(nums, begin, keyi - 1);
QuickSort(nums, keyi + 1, end);
}
快速排序的优化方案
1.在进行快速排序时,数据可能已经有序,此时快速排序的效率会被降低至 O(N2) 的量级,为了避免这种问题的出现,我们可以使用三数取中来解决,具体方法为:
选定区间最左侧和最右侧的值,求出可能的中间值的位置,将三个值进行比较,选出中间值,将中间值与第一个数据进行交换后再进行快速排序
2.在快速排序接近结束时区间内的数据量较少,继续使用快速排序效率会有所下降,可以使用直接插入排序来进行相应优化
优化后的快速排序
c
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//三数取中值
int findMid(int* nums, int begin, int end)
{
int midi = (begin + end) / 2;
if (nums[begin] < nums[midi])
{
if (nums[midi] < nums[end])
return midi;
else if (nums[end] > nums[begin])
return end;
else
return begin;
}
else
{
if (nums[midi] > nums[end])
return midi;
else if (nums[end] > nums[begin])
return begin;
else
return end;
}
}
//快速排序
void QuickSort(int* nums, int begin, int end)
{
//只有一个元素,或者没有元素时
if (begin >= end)
{
return;
}
//处理有序数组,找中间值
int midi = findMid(nums, begin, end);
Swap(&nums[midi], &nums[begin]);
//对最后的排序进行优化
if (end - begin + 1 < 10)
{
InsertSort(nums + begin, end - begin + 1);
}
int keyi = begin;
int left = begin;
int right = end;
while (left < right)
{
//找小
while (left < right && nums[right] >= nums[keyi])
{
right--;
}
//找大
while (left < right && nums[left] <= nums[keyi])
{
left++;
}
Swap(&nums[left], &nums[right]);
}
Swap(&nums[left], &nums[keyi]);
keyi = left;
QuickSort(nums, begin, keyi - 1);
QuickSort(nums, keyi + 1, end);
}
2.7 归并排序
如果有一组数据,它的左区间有序,它的右区间有序,那么就可以通过将左右区间的值进行比较的方式来使得数组有序
而归并排序的思想就是:
通过将一组数据划分为左右两个区间,对左右两个区间的数据分别进行排序,再合并左右两个区间的数据来使得一组数据有序
最后一趟排序如图,由于归并排序是递归实现的,所以它和其它几趟的排序类似
时间复杂度:O(NlogN)
空间复杂度:O(1)
稳定性:稳定
c
void _MergeSort(int* nums, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
//[begin, mid][mid+1, end]
//左区间归并
_MergeSort(nums, tmp, begin, mid);
//右区间归并
_MergeSort(nums, tmp, mid + 1, end);
//左右区间有序,进行归并
int leftBegin = begin, leftEnd = mid;
int rightBegin = mid + 1, rightEnd = end;
int i = begin;
while (leftBegin <= leftEnd && rightBegin <= rightEnd)
{
if (nums[leftBegin] <= nums[rightBegin])
{
tmp[i++] = nums[leftBegin++];
}
else
{
tmp[i++] = nums[rightBegin++];
}
}
while (leftBegin <= leftEnd)
{
tmp[i++] = nums[leftBegin++];
}
while (rightBegin <= rightEnd)
{
tmp[i++] = nums[rightBegin++];
}
memcpy(nums + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* nums, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(nums, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
2.8 计数排序
计数排序是一种不需要进行比较的排序方法,它的思想为:
将一组数据中每个值的出现次数记录下来,再通过它们的出现次数,向原数据写入对应数量的数据
在实现时,为了防止数据差过大,动态开辟过多的空间导致空间浪费,选择使用相对位置进行映射,做法为:
找出数组中最大和最小的值,确定要开辟的空间大小,再使用数组内的每个数减去最小值,获得相对位置,通过相对位置记录出现次数,再通过出现次数 + 最小值的方式,还原值并将值写入原数组
注:这里的蓝色箭头表示相对位置和值的改变情况
时间复杂度:O(N + range) -- range为count数组的大小
空间复杂度:O(range)
稳定性:稳定
c
void CountSort(int* nums, int n)
{
int max = nums[0];
int min = nums[0];
//找最大最小值
for (int i = 1;i < n;++i)
{
if (nums[i] < min)
min = nums[i];
if (nums[i] > max)
max = nums[i];
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
//记录出现次数
for (int i = 0;i < n;++i)
{
count[nums[i] - min]++;
}
//排序
int j = 0;
for (int i = 0;i < range;++i)
{
while (count[i]--)
{
nums[j++] = i + min;
}
}
}
3 排序的总结
