排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排
序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
常见的排序算法如图
插入排序
插入排序的动图如下
插入排序的思想逻辑很简单就是把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。插入排序可以理解为就是我们打扑克牌摸牌的过程,摸一张牌,依次比较然后将它插入的合适的位置。
这个排序很简单,根据图我们就可以把第一个数据当成有序的数据,然后后面的数据依次插入,直到将数据插入完,这样就有序了。
代码如下:
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; i++)
{
int endi = i;
while (endi)
{
if (a[endi - 1] > a[endi])
{
swap(&a[endi - 1], &a[endi]);
}
endi--;
}
}
}
总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
代码如下:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap>1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{ //+1是为了保证gap最后一次等于1
int endi = i;
int temp = a[endi+gap];
while (endi>=0)
{
if (a[endi] > temp)
{
swap(&a[endi+gap], &a[endi]);
endi -= gap;
}
else {
break;
}
a[endi + gap] = temp;
}
}
}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些书中给出的
希尔排序的时间复杂度都不固定。
《数据结构(C语言版)》— 严蔚敏
数据结构-用面相对象方法与C++描述》— 殷人昆
- 稳定性:不稳定
选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的
数据元素排完 。
直接选择排序
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
代码如下:
void SelectSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
int min = j;
for (int i = j; i < n; i++)
{
if (a[i] < a[min])
{
min = i;
}
}
swap(&a[j], &a[min]);
}
}
总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
冒泡排序
冒泡排序的核心思想就是两两比较,就大的(小的)数据放在后面
代码如下:
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
for (int i = 0; i < n - 1-j; i++)
{
if (a[i] > a[i + 1])
{
swap(&a[i], &a[i + 1]);
}
}
}
}
总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
void AdjustDwon(int* a, int n, int root)
{
int child = root * 2 + 1;
while (child < n)
{
if (a[child] < a[child + 1]&&child+1<n)
{
child++;
}
if (a[root] < a[child])
{
swap(&a[root], &a[child]);
root = child;
child = root * 2 + 1;
}
else
{
break;
}
}
}//向下调整
void HeapSort(int* a, int n)
{
for (int i = (n-1-1)/2; i >0; i--)
{ //向下调整建堆
AdjustDwon(a, n, i);
}
for (int i = n-1; i >0; i--)
{
swap(&a[0], &a[i]);//将最大的数于于根节点交换之后向下调整
AdjustDwon(a, i, 0);
}
}
总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中
的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右
子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序的基本结构
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a, left, right);
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
单趟排序有三种方法
1.Hoare版本
key这个数我们可以用三数取中的方法因为这样即使是在数据有序或者接近有序的情况下效率更高,而快排的核心思想就是R在右边,右边先动找比key小的值如果没有找到就向左移动,找到了就停下来,L在左边去找比key大的值,找到了并且没有与R相遇就交换,当R与L相遇时则就是key的所在位置
代码如下:
int GetMid(int* a, int begin, int end)
{
int mid = (begin + end)
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{return mid; }
else
{
if (a[begin] > a[end])
return begin;
else
return end;
}
}
else
{
if (a[mid] > a[end])
return mid;
else
{
if (a[begin] < a[end])
return begin;
else
return end;
}
}
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int midi = GetMid(a, left, right);
swap(&a[left], &a[midi]);
int begin = left;
int end = right;
int keyi = begin;
//三数取中
while (left <right)
{
while (left<right&&a[right] >= a[keyi])
{
right--;//右边找小
}
while (left<right&&a[left]<=a[keyi])
{
left++;//左边找大
}
swap(&a[left], &a[right]);
}
swap(&a[keyi], &a[left]);
return left;
}
为什么相遇位置一定是比key小的值
相遇无非就是两种情况,L遇到R,R遇到L,如果是L遇到R,我们让右边先走,R停下的位置一定是比key小的数,如果是R遇L,假设数组中的数都比key大,所以key遇到L是就是等于key,所以我们左边做key让右边先走,是可以保证相遇位置一定比key小的。
2.挖坑法
我们还是将左边做key,然后保存它的值,然后它就是一个坑,还是两个指针,由于左边有一个坑,所以右边就要找小的数来填这个坑,然后将右边的那个位置变成新的坑,然后左边找大,找到后接着填坑,更新坑的位置,L和R一定有一个是坑,所以,当他们相遇时,那个位置一定是坑,然后将key放进去即可。
代码如下:
int PartSort2(int* a, int left, int right)
{
int midi = GetMid(a, left, right);
swap(&a[midi], &a[left]);
int key = a[left];//保留key的值
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];//填坑
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3.前后指针法
定义两个指针一个prev一个cur,cur用来遍历数组,还是用左边的值来做key,然后将cur找到比key小的值就和++prev位置的数交换直到遍历结束,然后再把prev位置的值可key交换即可。
代码如下:
int PartSort3(int* a, int left, int right)
{
int midi = GetMid(a, left, right);
swap(&a[left], &a[midi]);
int prev = left;
int keyi = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
swap(&a[prev], &a[cur]);
cur++;
}
swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
代码如下:
void MergeSort(int* a, int right, int left,int*temp)
{
if (left >= right)
{
return;
}
int mid = (right + left) / 2;
MergeSort(a,left, mid,temp);
MergeSort(a,mid + 1, right,temp);
int begin1 = left;
int end1 = mid;
int begin2 = mid + 1;
int end2 = right;
int k = left;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
{
temp[k++] = arr[begin1++];
}
else
{
temp[k++] = arr[begin2++];
}
}
while (begin1 <= end1)
{
temp[k++] = a[begin1++];
}
while (begin2 <= end2)
{
temp[k++] = a[begin2++];
}
memcpy(arr + left, temp + left, (right - left + 1) * sizeof(int));
}
void _MergeSort(int* arr, int left, int right)
{
int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));
//不能在这个函数中递归,不然每次都要开辟数组
MergeSort(arr, left, right, tmp);
free(tmp);
tmp=NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
代码如下:
void CountSort(int* a, int n)
{
int min = a[0];
int max = a[0];
for (i = 0, i < n, i++)
{
if (min > a[i])
{
min = a[i];
}
if (max < a[i])
{
max = a[i];
}
}
int b = max - min + 1;
int* nums = (int*)malloc(sizeof(int) * c);
memset(nums, 0, c * sizeof(int));
//统计
for (int i = 0; i < n; i++)
{
nums[arr[i] - min]++;
}
int k = 0;
//拷贝回原数组
for (int i = 0; i < c; i++)
{
while (nums[i]--)
{
arr[k++] = i + min;
}
}
free(nums);
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)