C数据结构:排序
1.插入排序
2.希尔排序
3.选择排序
4.快速排序
5.归并排序
6.计数排序
7.排序总结
1.插入排序
数组arr的长度为n,假定下标[0,end]对应的元素有序,把arr[end+1]与下标为[0,end]对应的元素比较,从而找到合适位置插入arr[end+1],使[0,end+1]有序。
void InsertSort(int* a, int n)//插入排序
{
for (int i = 0;i < n - 1;i++)
{
int end = i;
int t = a[end + 1];
while (end >= 0)
{
if (t < a[end])
{
a[end + 1] = a[end];
--end;
}
else
break;
}
a[end + 1] = t;
}
}
例如数组arr[]={2,1,1,3}。
进入while循环,比较t与arr[end]的大小,执行if语句,把arr[end]的值覆盖到arr[end+1],end再自减1。
由于end<0,跳出while循环,把t赋值给此时的arr[end+1]。
进入下一次for循环,i变为1。
此时t又小于arr[end],再把arr[end]的值覆盖到arr[end]。
此时,t=arr[end],执行break,跳出while循环,再把t的值赋给arr[end+1]。
进入新一轮for循环,i为2,end更新为2,t变为3。
进入while循环,t>arr[end],执行break,跳出while循环,再把t赋给arr[end+1],arr[end+1]仍为3,排序完成。从这里也可看到,for循环的循环条件必须是i<n-1,若i<n,则i=n-1时,end+1就越界了。
插入排序的特性:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定(相同值在数组中相对顺序经排序后仍不变,则为稳定)
2.希尔排序
希尔排序是插入排序的优化,先选定一个整数gap,从首元素开始,每隔gap距离就把元素分为一组,然后控制gap进行循环,当gap>1时为预排序,让数组接近有序,当gap=1时,为插入排序。
void ShellSort(int* a, int n)//希尔排序
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//+1保证最后一个gap一定是1,gap>1时为预排序,gap=1时为插入排序
for (int i = 0;i < n - gap;i++)
{
int end = i;
int t = a[end + gap];
while (end >= 0)
{
if (t < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = t;
}
}
}
设数组a[10]={9,1,2,5,7,4,8,6,3,5},数组长度为10,即gap=10。进入while循环,gap变为4,即for循环的循环条件为i<6,进入for循环。
最终执行完一次for循环,数据变得有序。
接下来进入新一轮while循环,gap变为2,执行for循环。当再次进入while循环后gap变为1,我们把代码中for循环部分的gap全部换为1,就与插入排序的代码一样了。
希尔排序的特性:
- gap越大,大的数越快跳到后面,小的数越快跳到前面,相对越不接近有序
- gap越小,跳得越慢,但越接近有序
- 时间复杂度:O(N^1.3)
- 空间复杂度:O(1)
- 稳定性:不稳定
3.选择排序
选择排序遍历一遍数组,找出最小值,然后与首元素交换,除去首元素后再遍历一遍数组,找出最小值,与数组下标为1的元素交换,如此循环往复,直到只剩一个元素。优化后,我们可以一次遍历就取得最小最大值,分别放在数组的首尾,缩小区间,再次遍历。
void Swap(int* p1, int* p2)//交换
{
int t = *p1;
*p1 = *p2;
*p2 = t;
}
void SelectSort(int* a, int n)//选择排序
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin + 1;i <= end;i++)//循环一次,找到最大最小值,放到最左最右边,
{ //缩小区间,再进入新循环。
if (a[i] > a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
Swap(&a[begin], &a[mini]);
if (begin == maxi)
maxi = mini;
Swap(&a[end], &a[maxi]);
begin++;//缩小区间
end--;
}
}
以数组a[8]={9,1,2,5,7,4,6,3}为例。执行完第一次for循环后如图所示:
把数组中最小元素放到下标为0的位置出,即交换a[begin]和a[mini]。
此时,数组中最大值被换到了下标为mini的位置,如果直接交换a[maxi]和a[end],最小值就会被换到数组的最后,排序结果就会出错。所以需要判断当下标begin=maxi时,由于先执行了交换a[begin]和a[mini],最大值被换到了下标为mini的位置,要把maxi更新为mini,再交换a[maxi]和a[end]。
最后缩小区间,进入新一轮的while循环。
选择排序的特性:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
4.快速排序
hoare法
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
我们一般把首元素作为基准值。
void QuickSort1(int* a, int left, int right)//快速排序:hoare法
{
if (left >= right)//不存在的区间
return;
int key = left;
int begin = left, end = right;
while (begin < end)
{
while (begin < end && a[end] >= a[key])//end找比a[key]小的数
end--;
while (begin < end && a[begin] <= a[key])//begin找比a[key]大的数
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[key]);
key = begin; //[left,key-1] key [key+1,right]
QuickSort1(a, left, key - 1);
QuickSort1(a, key + 1, right);
}
以数组a[10]={6,1,2,7,9,3,4,5,10,8}为例,进入循环前如图所示。
当a[end]<a[key]时,end停下;当a[begin]>a[key]时,begin停下。此时交换a[end]和a[begin]。
end走到a[end]=3时,end停下,begin开始走,当begin=end时,不满足begin<end,begin停下,交换a[begin]和a[end],此时相当于自己和自己交换。
跳出外层while循环,交换a[key]和a[begin],让key=begin,分割出左右区间,再分别对两区间排序,即递归。
问题: 下标0做key,右边end先走时,为什么begin和end相遇位置一定比a[key]小?
若begin遇到end,因为end停下的位置对应的元素一定比a[key]小,所以相遇位置比a[key]小。若end遇到begin,end未遇到比a[key]小的元素就与begin相遇,由于经过上一轮交换,此时a[begin]<a[key],所以相遇位置比a[key]小。
相反,让最右边做key,左边begin先走,可保证相遇位置比a[key]大。
hoare法优化
三数取中: 当待排序数组已经是有序时,再使用快排会出现效率退化,end要走到最左边才能开始交换、递归。所以,我们在排序前取最左边元素a[left]、中间元素a[mid]、最右边元素a[right],比较后得到中间大小的元素,将它与首元素交换后作为a[key]。
int GetMidi(int* a, int left, int right)//三数取中
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else
return a[left] < a[right] ? right : left;
}
else
{
if (a[mid] > a[right])
return mid;
else
return a[left] < a[right] ? left : right;
}
}
小区间优化: 当区间内元素较少时,不必再递归分割排序,改为插入排序,可减少递归次数,防止栈溢出,并提升效率。
优化后的代码如下:
void QuickSort2(int* a, int left, int right)
{
if (left >= right)
return;
if ((right - left + 1) < 10)//小区间优化,当区间内元素较少时,不再递归分割排序。减少递归次数。
InsertSort(a + left, right - left + 1);
else
{
int begin = left, end = right;
int midi = GetMidi(a, left, right);//三数取中
Swap(&a[midi], &a[begin]);
int key = begin;
while (begin < end)
{
while (begin < end && a[end] >= a[key])
end--;
while (begin < end && a[begin] <= a[key])
begin++;
Swap(&a[begin], &a[end]);
}
Swap(&a[key], &a[begin]);
key = begin;
QuickSort2(a, left, key - 1);
QuickSort2(a, key + 1, right);
}
}
前后指针法
用prev、cur表示数组下标,初始时,prev指向首元素,cur指向prev指向的下一个元素。每次循环cur都要走一步,当a[cur]>=a[key]时,prev也走,当a[cur]<a[key],且prev走了一步后未与cur相遇时,交换a[prev]和a[cur]。直到cur越界时,停止循环,交换a[prev]和a[key]。
int PartSort(int* a, int left, int right)
{
int midi = GetMidi(a, left, right);//三数取中
Swap(&a[left], &a[midi]);
int key = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[key] && ++prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int key = PartSort(a, left, right);
QuickSort(a, left, key - 1);
QuickSort(a, key + 1, right);
}
仍以a[10]={6,1,2,7,9,3,4,5,10,8}为例。
如果a[cur]>a[key],则不执行交换,prev也不移动,这样的操作使prev和cur之间的元素全是大于a[key]的元素。
快排:非递归
我们使用数据结构中的栈,将区间[0,n]存入栈中,再将区间取出作为前后指针法函数的参数进行排序,接下来分出右左区间入栈,进入新循环。
void QuickSortNonR(int* a, int left, int right)//快排:非递归
{
ST st;//创建栈
STInit(&st);
STPush(&st, right);
STPush(&st, left);
while (!STEmpty(&st))//一次循环相当于一次递归
{
int begin = STTop(&st);
STPop(&st);
int end = STTop(&st);
STPop(&st);
int key = PartSort(a, begin, end);//前后指针,单趟排序
if (key + 1 < end)//右区间入栈
{
STPush(&st, end);
STPush(&st, key + 1);
}
if (begin < key - 1)//左区间入栈
{
STPush(&st, key - 1);
STPush(&st, begin);
}
}
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
5.归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
此算法要借助一个数组中转。
void _MergeSort(int* a, int* t, int begin, int end)//a中数据分解,谁小谁先放入t,最后把t拷贝给a
{
if (begin >= end)
return;
int mid = (begin + end) / 2;//后序遍历
_MergeSort(a, t, begin, mid);
_MergeSort(a, t, mid + 1, end);//[begin,mid]和[mid+1]区间内数据有序就开始归并
int begin1 = begin, end1 = mid;//归并
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
t[i++] = a[begin1++];
else
t[i++] = a[begin2++];
} //循环结束后,要么begin1走完,要么begin2走完
while (begin1 <= end1)//begin2走完,begin1未走完
t[i++] = a[begin1++];
while (begin2 <= end2)//begin1走完,begin2未走完
t[i++] = a[begin2++];
memcpy(a + begin, t + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)//归并排序
{
int* t = (int*)malloc(n * sizeof(int));
if (t == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, t, 0, n - 1);
free(t);
t = NULL;
}
归并排序:非递归
void MergeSortNonR(int* a, int n)//归并排序:非递归
{
int* t = (int*)malloc(n * sizeof(int));
if (t == NULL)
{
perror("malloc fail");
return;
}
int gap = 1;//一次归并中,参与归并的两组数据中每组有gap个数
while (gap < n)
{
for (int i = 0;i < n;i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;//区间[begin1,end1]
int begin2 = i + gap, end2 = i + 2 * gap - 1;//区间[begin2,end2]
if (begin2 >= n)//第二组区间都越界不存在,不用归并这一组
break;
if (end2 >= n)//第二组begin2没越界,end2越界,修正后归并
end2 = n - 1;
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
t[j++] = a[begin1++];
else
t[j++] = a[begin2++];
} //循环结束后,要么begin1走完,要么begin2走完
while (begin1 <= end1)//begin2走完,begin1未走完
t[j++] = a[begin1++];
while (begin2 <= end2)//begin1走完,begin2未走完
t[j++] = a[begin2++];
memcpy(a + i, t + i, (end2 - i + 1) * sizeof(int));
}
gap *= 2;
}
free(t);
t = NULL;
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
6.计数排序
计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
void CountSort(int* a, int n)//计数排序
{
int min = a[0], max = a[0];
for (int i = 1;i < n;i++)//找出a中最大最小元素
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
int range = max - min + 1;//待排序数组在区间[min,max]内有几个元素
int* count = (int*)calloc(range, sizeof(int));//count储存的是a中某元素在a出现的次数
if (count == NULL)
{
perror("calloc fail");
return;
}
for (int i = 0;i < n;i++)//a[i]-min使a中元素对应数组count的下标
count[a[i] - min]++; //a中某元素出现一次,count中对应下标的元素加1
int j = 0;
for (int i = 0;i < range;i++)
{
while (count[i]--)
a[j++] = i + min;
}
free(count);
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,range))
- 空间复杂度:O(range)
- 稳定性:稳定
7.排序总结
拙作一篇,望诸位同道不吝斧正。