本文介绍和实现常见排序算法,其中对详细介绍快速排序和归并排序递归与非递归版本的实现。
目录
一、插入排序
1.基本思想
2.直接插入排序
3.希尔排序
二、选择排序
1.基本思想
2.直接选择排序
3.堆排序
三、交换排序
1.基本思想:
2.冒泡排序
3.快速排序(递归实现)
1.hoare版本
2.挖坑版本
3.前后指针版本
4.快速排序优化
1.三数取中
2.小区间优化
5.快速排序(非递归实现)
四、归并排序
1.递归实现
2.非递归实现
1.思路一:
2.思路二:
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想:
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
下图以排升序为例:
动图演示:
代码:
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)//如果插入元素小于当前比较元素(排升序)
{
a[end + 1] = a[end];//大的元素向后移动一位
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
(排序后不会打乱相同数据在数组中前后位置,则为稳定,希尔排序图解中标记)
希尔排序法又称缩小增量法。
希尔排序法的基本思想:
1.先选定一个小于N的整数gap作为第一增量,然后将所有距离为gap的元素分在同一组,并对每一组的元素进行直接插入排序。然后再取一个比第一增量小的整数作为第二增量,重复上述操作…
2.当增量的大小减到1时,就相当于整个序列被分到一组,进行一次直接插入排序,排序完成。
当gap越大时,数据挪动的越快,可以让数据快速到达所对应的位置附近。
下图以排升序为例:(gap每次除2)
第一趟:gap = 5,分为5组元素,每组两个,对每一组进行插入排序
第二趟:gap = 2,分为两组,每组5个元素,对每一组进行插入排序
第二趟:gap = 1,分为1组,进行插入排序
代码:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//当gap除以的数大于2时,+1避免出现gap/3=0的情况
//对每组进行插入排序
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. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,暂且按照O(N*logN)。
4.空间复杂度:O( 1 )。
5.稳定性:不稳定(如上图中演示中:带下划线的5经过排序后被移动到了不带下划线的5的前面)
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
1.在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素。
2.若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
3.在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
动图演示:
代码:
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int begin = i;
int min = begin;
while (begin < n)//遍历数组寻找最小的元素
{
if (a[begin] < a[min])
{
min = begin;
}
begin++;
}
Swap(&a[i], &a[min]);//交换
}
}
直接选择排序的特性总结:
1. 直接选择排序思想非常好理解,但是效率不是很好,实际中很少使用。
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
1.建堆:(对数组进行向下调整)
①升序:建大堆
②降序:建小堆
2. 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,用向下调整完成堆排序。
下图以排升序为例:
建立大堆,将最大的数与堆最后一个元素交换,size-1(此处不是删除数据,将最大的值排到最后,size-1后,该值不再参与向下调整),再从根节点向下排序选出次大值循环。
当前堆为建好的大堆:
代码:
void Swap(int* p1, int* p2)//交换数据
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(int*a, int size, int parent)//向下调整
{ //size为当前堆(需要调整的)元素个数 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)//a为数组指针,n为数组的元素个数
{ //先用向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end>0)//将堆顶数据放到数组尾部,再次向下调整
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排 序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
动图演示:(升序)
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void BubbleSort(int* a, int n)
{
assert(a);
for (int j = 0; j < n; 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. 稳定性:稳定
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
步骤:(以排升序为例)
1.先从数列中取出一个数作为基准数key(一般取最左边或者最右边的值)。
2.分区过程,从最右边向左寻找比key小的值 与 从最左边向右寻找比key大的值交换。两者相遇的位置与key交换。将比这个数key大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数(递归)。
此时,key的左侧都比key小,右侧都比key大
再将左右区间重复第二步,直到各区间只有一个数(递归)。
代码:
int PartSort1(int* a, int begin, int end)
{
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]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)//此时只有一个数据或者区间不存在
{
return;
} //hoare版本
int keyi = PartSort1(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
步骤:(以排升序为例)
1.将基准数key挖出形成第一个坑a[L]。
2.R--由后向前找比它小的数,找到后挖出此数填前一个坑a[L]中。
3.L++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[R]中。
4.再重复执行2,3二步,直到L==R,将基准数填入a[L]中。
再对key的左右区间递归。
代码:
int PartSort2(int* a, int begin, int end)
{
int left = begin, right = end;
int piti = begin;
int key = a[piti];
while (left < right)
{
while (left < right && a[right] >= key)
{
--right;
}
a[piti] = a[right];
piti = right;
while (left < right && a[left] <= key)
{
++left;
}
a[piti] = a[left];
piti = left;
}
a[piti] = key;
return piti;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort2(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
步骤:(以排升序为例)
1.选出基准数key
2.prev指针指向序列开头,cur指针指向prev+1的数据。
3.若cur指向的内容小于key,则prev先向后移动一位,然后交换prev和cur指针指向的内容,然后cur指针++。
若cur指向的内容大于key,则cur指针直接++。如此进行下去,直到cur指针越界,此时将key和prev指针指向的内容交换即可。
再对key的左右区间递归。
代码:
int PartSort3(int* a, int begin, int end)
{
int prev = begin;
int key = begin;
int cur = begin+1;
int mid = GetMidIndex(a, begin, end);//三数取中(见下文)
Swap(&a[key], &a[mid]);//三数取中
while (cur <= end)
{
if (a[cur] < a[key] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
++cur;
}
Swap(&a[key], &a[prev]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if (end - begin < 10)//小区间优化(见下文)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
在理想情况下,快速排序的时间复杂度是O(N*logN)。
理想情况下,key的大小每次都是中间值,key左右两侧的区间长度相等。
但是当序列有序或者接近有序时,我们取最左边或最右边的数据就会取到最小或者最大值,此时的时间复杂度为O(N^2),当数据过多时还可能会栈溢出。
为了避免上述情况,我们取序列的最左、最右和中间的数据比较,却中间大小的数据作为key值。
代码:
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[end])
{
if (a[begin] > a[mid])
return begin;
else if (a[mid] < a[end])
return mid;
else
return end;
}
else
{
if (a[mid] > a[begin])
return begin;
else if (a[mid] > a[end])
return mid;
else
return end;
}
}
在上文的前后指针版本中有使用。
当待排序序列的数据个数较少时,我们遍不再使用快速排序对小区间进行递归,可以考虑用其他排序。小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。
如:假设区间小于10,不再递归排序小区间,将减少80%以上的递归次数。
参考上文中的前后指针版本的代码:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if (end - begin < 10)//小区间优化
//当区间内数据小于10个,则使用插入排序
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
快速排序的排序的非递归,需要借助栈来模拟递归。
步骤:
1.将需要排序的序列的左右区间按读取顺序入栈。
2.当栈不为空时,读取栈中左右区间的值,用上述3个版本中任意方法排序,获取key的值。比较key和左右区间的值,当key大于或等左或右区间的值时,说明该区间只有1个或不存在,此时该区间有序。若key小于左或右区间的值,将左区间--key 和key--右区间的值按读取顺序入栈。
3.重复步骤2,直至栈为空。
代码:
栈:(Stack.h)
#include
#include
#include
#include
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void StackInit(ST* ps);
void StackDestory(ST* ps);
void StackPush(ST* ps, STDataType x);
void StackPop(ST* ps);
STDataType StackTop(ST* ps);
bool StackEmpty(ST* ps);
int StackSize(ST* ps);
(Stack.c)
#include"stack.h"
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
void StackPop(ST* ps)
{
assert(ps);
assert(!(StackEmpty(ps)));
ps->top--;
}
STDataType StackTop(ST* ps)
{
assert(ps);
assert(!(StackEmpty(ps)));
return ps->a[ps->top - 1];
}
bool StackEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
快速排序非递归的三个版本与递归版本相同,直接引用即可。
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);
//PartSort1、PartSort2 自行替换即可
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyi+1);
}
if (keyi - 1 > left)
{
StackPush(&st, keyi-1);
StackPush(&st, left);
}
}
StackDestory(&st);
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
(以排升序为例)
将序列划分为左右两个子区间,进行递归,当左右两个子区间只有一个数据或不存在是,即为有序,对两子区间进行合并。
进行归并排序需要创建一个与原序列大小相同的空间,用于存放合并后的数据。合并完成后,拷贝回原序列。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)//当区间只有1个数据或不存在时,直接返回
return;
int mid = (begin + end) / 2;
//递归
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
//归并
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, sizeof(int)*(end-begin+1));
}
归并排序的非递归实现只需要对原序列分组进行归并即可。
当原序列数据个数正好为2的次方个时,上述情况才能正常运行。
当原序列数据个数不为2的次方个时,则需要对分组后的区间进行修正。
将所有数据合并过一次后,统一拷贝回原序列。
区间修正,以越界部分为例:
其中当gap=1时,每个数据为一组,begin1一定不会越界。
当end1>=n时,越界,end1置为n-1。begin2置为n,end2置为n-1,即该区间不存在。
当begin2>=n时,begin2置为n,end2置为n-1,即该区间不存在。
当只有end2>=n时,end2置为n-1。
代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc");
exit(-1);
}
int gap = 1;
while (gap= n)
{
end1 = n - 1;
begin2 = n;
end2 = n - 1;
}
else if(begin2>=n)
{
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
//归并
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[tmpi++] = a[begin1++];
else
tmp[tmpi++] = a[begin2++];
}
while (begin1 <= end1)
{
tmp[tmpi++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[tmpi++] = a[begin2++];
}
}
memcpy(a, tmp, sizeof(int) * n);//合并后拷贝回原序列
gap*=2;
}
free(tmp);
}
只要有归并两组数据合并,就将其拷贝回原序列。
当end1>=n时,此时只有左区间存在,且已经合并到序列最后,直接跳出当前循环即可。
当begin2>=n时,同上。
当只有end2>=n时,此时左右区间都存在,需要合并,将end2置为n-1即可。
代码:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc");
exit(-1);
}
int gap = 1;
while (gap < n)
{
int begin1, end1, begin2, end2;
for (int i = 0; i < n; i += 2 * gap)
{
begin1 = i;
end1 = i + gap - 1;
begin2 = i + gap;
end2 = i + 2 * gap - 1;
int tmpi = begin1;
//区间修正
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
int m = end2 - begin1 + 1;//记录当前左右两个区间的数据个数
//归并
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[tmpi++] = a[begin1++];
else
tmp[tmpi++] = a[begin2++];
}
while (begin1 <= end1)
{
tmp[tmpi++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[tmpi++] = a[begin2++];
}
memcpy(a+i, tmp+i, sizeof(int) * m);
}
gap *= 2;
}
free(tmp);
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定