文章目录
一、排序
1.排序概念
什么是排序: 所谓排序,就是将杂乱无章的数据元素,通过一定的方法按关键字按照递增或递减的顺序排列起来的操作过程。
排序的稳定性: 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次 序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排 序算法是稳定的;否则称为不稳定的。
内部排序: 待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程。
外部排序: 待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。
衡量效率的方法
内部排序: 比较次数,也就是时间复杂度
外部排序: IO次数,也就是读写外存的次数
2.术语说明
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;内排序:所有排序操作都在内存中完成;外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;时间复杂度: 一个算法执行所耗费的时间。空间复杂度:运行完一个程序所需内存的大小。
3.排序分类
常见排序算法
什么是接近有序?
小的元素尽量靠前,大的元素尽量靠后,不大不小的元素在中间。
1.插入排序:
1.1代码实现
vector<T> bInsertSort(vector<T> &arr)
{
int size = arr.size();
for (int i = 1; i < size; i++)
{
int val = arr[i];
int end = i - 1;
while(end>=0&&val<arr[end])
{
arr[end+1] = arr[end];
end--;
}
arr[end + 1] = val;
}
return arr;
}
插入排序的特性总结:
- 元素集合越接近有序,数据量比较少,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
2.希尔排序:
2.1代码实现
当gap=gap/3+1时性能最高
vector<T> ShellSort(vector<T> &arr)
{
int size = arr.size();
int gap=size/2;
for (gap = size / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < size; i++)
{
int val = arr[i];
int end = i - gap;
while (end >= 0 && val < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = val;
}
}
return arr;
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
- 稳定性:不稳定
3.选择排序:
3.1代码实现
vector<T> selectSort(vector<T> &arr)
{
for (int i = 0; i < arr.size() - 1; i++)
{
int maxpos = i;
for (int j = i + 1; j < arr.size(); j++)
{
if (arr[maxpos] > arr[j])
{
maxpos = j;
}
}
if (maxpos != i)
{
int temp = arr[i];
arr[i] = arr[maxpos];
arr[maxpos] = temp;
}
}
return arr;
}
代码优化:
vector<T> selectSort_OP(vector<T> &arr)
{
int size = arr.size();
int begin = 0;
int end = size - 1;
//在区间中找到最大和最小元素的位置
while (begin < end)
{
int maxpos = begin;
int minpos = begin;
int i = begin + 1;
while (i <= end)
{
if (arr[i] > arr[maxpos])
maxpos = i;
if (arr[i] < arr[minpos])
minpos = i;
i++;
}
if (maxpos != end)
{
int temp = arr[end];
arr[end] = arr[maxpos];
arr[maxpos] = temp;
}
if (minpos == end)
{
minpos = maxpos;
}
if (minpos != begin)
{
int temp = arr[begin];
arr[begin] = arr[minpos];
arr[minpos] = temp;
}
begin++;
end--;
}
return arr;
}
选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
4.堆排序:
4.1代码实现
void Swap(T *s1,T *s2)
{
T temp = *s1;
*s1 = *s2;
*s2 = temp;
}
void HeapAdjust(vector<T> &arr, int size, int parent)
{
//默认child标记左孩子
int child = parent * 2 + 1;
while (child < size)//左孩子存在找
{
//找左右孩子中最大的(大堆)
if (child + 1 < size&&arr[child + 1] > arr[child])
child += 1;
//检测双亲是否满足堆的性质
if (arr[child] > arr[parent])
{
Swap(&arr[child],&arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
vector<T> heapSort(vector<T> &arr)
{
//建堆--升序(大堆) 降序(小堆)
//从倒数第一个非叶子节点--向下调整
int size = arr.size();
int last = (size - 2) >> 1;//根据节点求双亲
for (; last >= 0; last--)
{
HeapAdjust(arr, size, last);
}
//排序--堆的删除
int end = size - 1;
while (end)
{
Swap(&arr[0], &arr[end]);
HeapAdjust(arr, end, 0);
--end;
}
return arr;
}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
5.冒泡排序:
5.1代码实现
vector<T> bubleSort(vector<T> &arr)
{
int size = arr.size();
for (int i = 0; i < size; i++)
{
for (int j = 0; j < size-i-1; j++)
{
if (arr[j]>arr[j+1])
{
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
6.快速排序:
6.1代码实现
void Swap(T *s1, T *s2)
{
int temp = *s1;
*s1 = *s2;
*s2 = temp;
}
T Partion1(vector<T> &arr, T left, T right)
{
int key = arr[right - 1];//基准值
int begin = left;
int end = right - 1;
while (begin<end)
{
//begin从前往后找比基准值大的元素,找到就停下来
while (begin<end&&arr[begin] <=key)
begin++;
//end从后往前找比基准值小的元素,找到就停下来
while (begin<end&&arr[end] >=key)
end--;
if(begin<end)
Swap(&arr[begin], &arr[end]);
}
if (begin != right - 1)
Swap(&arr[begin], &arr[right - 1]);//当begin和end相等时,证明begin左边都是比key小的数,右边都是比key大的数,所以交换begin或者end和最后一个元素基准值位置的元素
return begin;
}
void QuickSort(vector<T> &arr,T left,T right)
{
if (right - left > 1)
{
int div = Partion1(arr, left, right);
QuickSort(arr, left, div);
QuickSort(arr, div, right);
}
}
第二种:挖坑法(分区函数不一样)
T Partion2(vector<T> &arr, T left, T right)//左右基准值分区
{
int key = arr[right - 1];//基准值
int begin = left;
int end = right - 1;
while (begin<end)
{
//begin从前往后找比基准值大的元素,找到就停下来
while (begin<end&&arr[begin] <=key)
begin++;
if (begin < end)
{
arr[end] = arr[begin];
end--;
}
//end从后往前找比基准值小的元素,找到就停下来
while (begin<end&&arr[end] >=key)
end--;
if (begin<end)
{
arr[begin] = arr[end];
begin++;
}
}
arr[begin] = key;//最后填坑
return begin;
}
第三种:
T Partion3(vector<T> &arr, T left, T right)//左右基准值分区
{
int key = arr[right - 1];//基准值
int cur = left;
int pre = left-1;
while (cur < right)
{
if (arr[cur] < key&&++pre != cur)
Swap(&arr[pre], &arr[cur]);
++cur;
}
if (++pre!=right - 1)
Swap(&arr[pre], &arr[right - 1]);
return pre;
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
7.归并排序:
7.1代码实现
vector<T> mergeSort(vector<T>&v)
{
bool Invalid_Input = false;
vector<T>copy;
int len = v.size();
if (len <= 0) {
Invalid_Input = false;
return copy;
}
for (int i = 0; i < len; i++)
copy.push_back(v[i]);
mergeSortHelp(v, copy, 0, len - 1);
return copy;
}
void mergeSortHelp(vector<T>&data, vector<T>©, int start, int end) {
if (start == end)
return;
int mid = (start + end) / 2;
mergeSortHelp(copy, data, start, mid);
mergeSortHelp(copy, data, mid + 1, end);
int i = mid, j = end;
int index = end;
while (i >= start && j >= mid + 1) {
if (data[i] > data[j])
copy[index--] = data[i--]; //从小到大
//copy[index--] = data[j--];从大到小
else
copy[index--] = data[j--];//从小到大
//copy[index--] = data[i--];从大到小
}
for (; i >= start;)
copy[index--] = data[i--];
for (; j >= mid + 1; j--)
copy[index--] = data[j--];
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
8.基数排序:
8.1代码实现
void radixSort(vector<int> &arr, int n)
{
int i, j, k, pos, num, index;
//这几句话是创建一个从0-9(行)× (n+1)(列)的网格,第一列从上往下是0-9,
//第二列是该行包含的元素个数,默认为0个
int *radixArrays[10];
for (i = 0; i < 10; i++) {
radixArrays[i] = (int *)malloc(sizeof(int) * (n + 1));
radixArrays[i][0] = 0;
}
//pos最大为31为数,计算机能承受的最大范围了
for (pos = 1; pos <= 31; pos++) {
//该for循环是将数组的元素按照位数(pos)的值放进网格内
for (i = 0; i < n; i++) {
num = getNumPos(arr[i], pos);
index = ++radixArrays[num][0];
radixArrays[num][index] = arr[i];
}
//该for循环是将上面的for循环已经按照某个位数(pos)排列好的元素存入数组
for (i = 0, j = 0; i < 10; i++) {
for (k = 1; k <= radixArrays[i][0]; k++) {
arr[j++] = radixArrays[i][k];
}
//清空网格,以便给下个位数排列
radixArrays[i][0] = 0;
}
}
}
int getNumPos(int num, int pos)
{
int i;
int temp = 1;
for (i = 0; i < pos - 1; i++) {
temp *= 10;
}
return (num / temp) % 10;
}
基数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(n+k)
- 空间复杂度:O(n+k)
- 稳定性:稳定
9.计数排序:
9.1代码实现
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
4.算法总结
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | In-place | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | In-place | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | In-place | 稳定 |
希尔排序 | O(n log n) | O(n log^2 n) | O(n log^2 n) | O(1) | In-place | 不稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | Out-place | 稳定 |
快速排序 | O(n log n) | O(n log n) | O(n^2) | O(log n) | In-place | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n^2) | O(n+k) | Out-place | 稳定 |
基数排序 | O(n+k) | O(n+k) | O(n* k) | O(n+k) | Out-place | 稳定 |
图表名词解释:
n: 数据规模k: “桶”的个数
In-place: 占用常数内存,不占用额外内存
Out-place: 占用额外内存