目录
?1. 排序概念
2. 常见的排序算法的实现
2.1 插入排序
?2.1.1 直接插入排序
2.1.2 希尔排序(缩小增量排序)
2.2 选择排序
2.2.1?基本思想:
2.2.2 直接选择排序
2.2.3 堆排序
2.3 交换排序
2.3.1 冒泡排序
2.3.2 快速排序
2.3.3 快速排序优化
2.3.4 快速排序的非递归实现
2.4 归并排序
2.5 非比较排序
3.排序算法复杂度及稳定性分析
?编辑
?1. 排序概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2. 常见的排序算法的实现
2.1 插入排序
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时,就用了插入排序的思想
?2.1.1 直接插入排序
? ? ? ? ?将 1?个数据插入 n 个有序数据中,使得这 n+1 个数据有序:比如有 n 个数据要排序,先将1个数据插入1个数据,使得这两个数据有序,然后插入第3个数据,使得这3个数据有序,以此类推,直到插入完 n?个数据,使得 n 个数据有序。
代码:
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i)
{
// [0,end]有序,把end+1位置的值插入,保持有序
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
????????1. 元素集合越接近有序,直接插入排序算法的时间效率越高 ????????2. 时间复杂度:O(N^2) ????????3. 空间复杂度:O(1),它是一种稳定的排序算法 ????????4. 稳定性:稳定
2.1.2 希尔排序(缩小增量排序)
原理:一个叫希尔的人改进了直接插入排序,因为元素集合越接近有序,插入排序算法的时间效率越高,所以他将排序分为两步:1· 预排序(使元素集合接近有序)? 2·直接插入排序(使之有序)
预排序:将元素集合分为gap组,即距离为gap的元素在同一组,对每一组元素进行直接插入排序,改变gap,重复上述操作,直至gap==1,即为直接插入排序,排序结束。
代码实现:
void ShellSort(int* a, int n)
{
// gap > 1时是预排序
// gap 最后一次等于1,是直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//+1是保障gap最后可以取到1
//每组直接插入排序
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
希尔排序的特性总结:
1. 排升序,gap越大,大的数更快到后面,小的数可以更快的到前面,但是越不接近有序 ? ? 排升序,gap越小,越接近有序的,当gap == 1,就是直接插入排序
2. 希尔排序是对直接插入排序的优化
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
4. 稳定性:不稳定 ?
2.2 选择排序
2.2.1?基本思想:
????????每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
2.2.2 直接选择排序
? ? ? ?步骤:
????????~在元素集合中选择最大(小)的数据元素
? ? ? ? ~若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
? ? ? ? ~在剩余的元素集合中,重复上述步骤,直到集合剩余1个元素
void SelectSort(int* a, int n)
{
assert(a);
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
Swap(&a[begin], &a[mini]);
// 如果begin和maxi重叠,那么要修正一下maxi的位置
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:不稳定
2.2.3 堆排序
????????堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDwon(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 降序 -- 建小堆
// 升序 -- 建大堆
void HeapSort(int* a, int n)
{
// 建堆方式:O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
// O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
堆排序特性总结:
????????1. 堆排序使用堆来选数,效率就高了很多。 ????????2. 时间复杂度:O(N*logN) ????????3. 空间复杂度:O(1) ????????4. 稳定性:不稳定
2.3 交换排序
????????基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1 冒泡排序
void BubbleSort(int* a, int n)
{
assert(a);
for (int j = 0; j < n - 1; ++j)
{
int exchange = 0;
for (int i = 1; i < n - j; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:稳定
2.3.2 快速排序
????????快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
1. hoare版本
思想:
首先单趟排序:选出一个key,一般为最左边或者最右边的值。要求单趟排完以后:左边比key小,右边比key大
?方法:以选最左边值为key为例:右边先走,找比key小的值,然后左边再走,找比key大的值,然后交换,重复上述过程直到左右相遇,然后把相遇位置的值与最左边也就是key值交换。
注:选最左边值为key,为什么要右边先走,能不能左边先走?
答:不能,右边先走是为了保证相遇位置值,比key小,先走左边就不一定了。
????????单趟排完以后,key左边值都比key小,右边值都比key大,接下来只要把key左右区间再重复单趟排序直至整体有序就行了。
void QuickSort(int* a, int begin, int end)
{
// 区间不存在,或者只有一个值则不需要在处理
if (begin >= end)
{
return;
}
int left = begin, right = end;
int keyi = left;
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]);
keyi = left;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
2. 挖坑法
思路如下列图:
void QuickSort(int* a, int begin, int end)
{
// 区间不存在,或者只有一个值则不需要在处理
if (begin >= end)
{
return;
}
int key = a[begin];
int piti = begin;//坑
while (begin < end)
{
// 右边找小,填到左边的坑里面去。这个位置形成新的坑
while (begin < end && a[end] >= key)
{
--end;
}
a[piti] = a[end];
piti = end;
// 左边找大,填到右边的坑里面去。这个位置形成新的坑
while (begin < end && a[begin] <= key)
{
++begin;
}
a[piti] = a[begin];
piti = begin;
}
a[piti] = key;
int keyi = piti;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
3. 前后指针版本
思路见图:
void QuickSort(int* a, int begin, int end)
{
// 区间不存在,或者只有一个值则不需要在处理
if (begin >= end)
{
return;
}
int prev = begin;
int cur = begin + 1;
int keyi = begin;
while (cur <= end)
{
// cur位置的之小于keyi位置值
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
2.3.3 快速排序优化
1. 三数取中
? ? ? ? 我们发现,上面三种快速排序的思路类似,都要选取一个key值,但是key值的选取不同,排序效率也不同,最优情况为每次key都恰好取中间值,这样一直二分是效率最高的,但是如果开始选取的key为最大值或者最小值,那么效率就会大大下降。
? ? ? ? 因此,为了提高效率,我们采用三数取中的方法来优化:在第一个 中间 最后一个数中取不是最大也不是最小的值作为key。
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return end;
}
else
{
return begin;
}
}
else // (a[begin] >= a[mid])
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
void QuickSort(int* a, int begin, int end)
{
// 区间不存在,或者只有一个值则不需要在处理
if (begin >= end)
{
return;
}
int prev = begin;
int cur = begin + 1;
int keyi = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
// cur位置的之小于keyi位置值
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
2.?递归到小的子区间时,可以考虑使用插入排序
这种优化可以极大减少递归次数,从而提高效率。
void QuickSort(int* a, int begin, int end)
{
// 区间不存在,或者只有一个值则不需要在处理
if (begin >= end)
{
return;
}
if (end - begin > 10)//假设区间大于10才递归
{
int prev = begin;
int cur = begin + 1;
int keyi = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
// cur位置的之小于keyi位置值
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else
{
InsertSort(a+begin, end - begin + 1);
}
}
2.3.4 快速排序的非递归实现
快速排序的非递归实现是利用栈来实现的,C语言实现栈见该博客《C语言实现栈》。
思路:将区间的end和begin依次入栈,然后依次出栈并且把子区间入栈,直至栈为空。
?
代码实现:
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int prev = begin;
int cur = begin + 1;
int keyi = begin;
// 加入三数取中的优化
int midi = GetMidIndex(a, begin, end);
Swap(&a[keyi], &a[midi]);
while (cur <= end)
{
// cur位置的之小于keyi位置值
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
// [left, keyi-1] keyi[keyi+1, right]
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st,keyi + 1);
}
if (left < keyi - 1)
{
StackPush(&st, keyi - 1);
StackPush(&st, left);
}
}
StackDestroy(&st);
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(logN) 4. 稳定性:不稳定 ?
2.4 归并排序
基本思想: ????????归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。
? ? ? ? 主要过程:将已有序列分为多个子序列,然后将子序列逐个合并,并使得合并后的序列有序。
?代码实现:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end] 分治递归,让子区间有序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
//归并 [begin, mid] [mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 把归并数据拷贝回原数组
memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(N) 4. 稳定性:稳定 ?
2.5 非比较排序
这里我们只介绍常用的计数排序。
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤: ????????1. 统计相同元素出现次数 ????????2. 根据统计的结果将序列回收到原来的序列中
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 1; i < n; ++i)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
// 统计次数的数组
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int)*range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memset(count, 0, sizeof(int)*range);
// 统计次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
// 回写-排序
int j = 0;
for (int i = 0; i < range; ++i)
{
// 出现几次就会回写几个i+min
while (count[i]--)
{
a[j++] = i + min;
}
}
}
局限性:
1. 如果数据是 浮点数,字符串就不能玩了。
2. 如果数据范围很大,空间复杂度很高,就不合适了。
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 2. 时间复杂度:O(MAX(N,范围)) 3. 空间复杂度:O(范围)
3.排序算法复杂度及稳定性分析
?
|