数据结构-排序算法(C语言实现)
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。对于不同场景下采用不同的排序算法其时间复杂度和空间复杂度都能得到大大提高。
前言
排序稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
编译环境:vs2013
一、算法介绍
对于所有排序,统一以数组array[size-1]为待排序对象。size为数组array元素个数。
1.插入排序
1.排序思想
将关键值key插入到一元素个数为size的有序数组array中。
从区间末端开始依次和关键值key进行比较,若array[size-1]>key,则将array[size-1]后移一个位置,依次往前将key与数组元素进行比较,当key值大时直接插入到有序数组中。
对于数组array[size-1]插入排序过程:
第一趟:默认第一个元素与自己有序,将第二个元素看做带插入关键值key。
第二趟:数组前两个元素已经有序,将第三个元素看做将插入已有序的前两个元素中的key值。
以此类推,即插入排序。
例子:玩扑克牌时对牌进行大小排序。
2.特性说明
算法效率:元素集合越接近有序,直接插入排序算法的时间效率越高
时间复杂度: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)
算法稳定性:稳定
3.算法实现
void Swap(int *left, int *right)
{
int temp = *left;
*left = *right;
*right = temp;
}
void InsertSort(int array[], int size)
{
for (int i = 1; i < size; ++i)
{
int key = array[i];//依次将关键值获取到
int end = i - 1;
//找插入元素在array中的位置
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
//进行元素key的插入
array[end + 1] = key;
}
}
2.希尔排序
1.排序思想
先选定一个整数grap,把待排序数组中所有记录分成grap个组,所有距离为grap的数分在同一组内,并对每一组内的记录进行排序。然后,再取grap,重复上述分组和排序的工作。当到达grap=1时,所有记录在统一组内便排好序。
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。
而对于grap的选取,我们采用Knuth提出的grap=grap/3+1,参考于《数据结构-用面相对象方法与C++描述》— 殷人昆。2.特性说明
时间复杂度:O(N^1.25) ~ O(1.6N^1.25)
空间复杂度:O(1)
算法稳定性:不稳定
3.算法实现
void ShellSort(int array[], int size)
{
int grap = size;
while (grap > 1)
{
grap = grap / 3 + 1;
for (int i = grap; i < size; ++i)
{
//单个元素的互换
int key = array[i];
int end = i - grap;
//找待插入元素在array中的位置
while (end >= 0 && key < array[end])
{
//将元素往后搬移到当前分组的下一个位置
array[end + grap] = array[end];
//获取当前分组的前一个元素位置
end -= grap;
}
//进行元素的插入
array[end + grap] = key;
}
}
}
3.选择排序
1.排序思想
待排数组array[size-1],遍历数组找出最大元素的位置下标,将最大元素与数组最后一个元素进行数据交换。
2.特性说明
时间复杂度: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)
算法稳定性:不稳定
选择排序算法实现
void SelectSort(int array[], int size)
{
//控制循环的趟数
for (int i = 0; i < size; ++i)
{
//遍历数组找到最大元素的下标
int maxPos = 0;
for (int j = 1; j < size-i; ++j)
{
if (array[j]>array[maxPos])
maxPos = j;
}
//每一趟后,将最大元素和区间的最后一个位置
if (maxPos != size - i - 1)
{
Swap(&array[maxPos], &array[size - i - 1]);
}
}
}
3.选择排序优化
在前面的选择排序思想中,我们在对数组进行遍历一次找到最大元素的下标位置,然后将最大元素与数组最后一个元素进行交换,即一趟只能排序好一个元素。
(ˇˍˇ) 想想~:那既然一趟可以找到最大元素的位置,也可以找到最小元素的位置,最大元素放数组末尾,最小元素放初始位置,那一次便可以排好两个元素。
优化选择排序算法实现
void SelectSortOP(int array[], int size)
{
int begin = 0;
int end = size - 1;
while (begin < end)
{
int maxPos = begin;
int minPos = begin;
int index = begin + 1;
//找出区间中最大和最小的元素
while (index <= end)
{
if (array[index]>array[maxPos])
maxPos = index;
if (array[index]<array[minPos])
minPos = index;
++index;
}
//找到最大最小的元素后,将最大元素往区间最末尾存放
if (maxPos != end)
{
Swap(&array[maxPos], &array[end]);
}
//若最小元素恰好在end的位置,此时交换结束后得对minPos进行更新
if (minPos == end)
{
minPos = maxPos;
}
if (minPos != begin)
{
Swap(&array[minPos], &array[begin]);
}
begin++;
end--;
}
}
4.选择排序再优化-堆排序
每次在对数组进行最大小元素的查找时,进行了一些数据的重复性比较,即查找元素的时间复杂度为O(n),这就降低了算法的性能。
故:我们可以采用堆排序来对选择排序再优化!
建堆:升序建大堆,降序建小堆。
利用堆的思想进行排序:利用堆的删除思想,将堆顶元素与最后一个元素进行交换,将堆中元素个数减少一个,再将堆进行向下调整。
时间复杂度: O ( N ∗ l o g 2 N ) O(N*log_2N) O(N∗log2N)
空间复杂度: O ( 1 ) O(1) O(1)
算法稳定性:不稳定
堆排序使用堆来选数,效率就高了很多。
选择排序再优化-堆排序算法实现
//堆的向下调整
//时间复杂度:O(logN)
void HeapAdjust(int array[], int size, int parent)
{
int child = 2 * parent + 1;
while (child < size)
{
//右孩子存在的情况下需要找到左右孩子中较大的孩子
if (child + 1 < size && array[child + 1] > array[child])
child += 1;
//检测parent是否符合堆的性质
if (array[parent] < array[child])
{
Swap(&array[parent], &array[child]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
//堆排序
void HeapSort(int array[], int size)
{
//1.对排序数组进行建堆,升序建大堆,降序建小堆
//建大堆
for (int root = (size - 2) / 2; root >= 0; --root)
{
HeapAdjust(array, size, root);
}
//2.堆删除思想进行排序
//将堆顶元素和堆中最后一个元素进行交换,有效元素-1,堆顶元素向下进行调整
int end = size - 1;
while (end)
{
Swap(&array[0], &array[end]);
HeapAdjust(array, end, 0);
end--;
}
}
4.冒泡排序
1.排序思想
从初始位置开始与相邻的元素依次往后比较,若大于后一个元素的值则进行交换,不大于后一个元素又和其后一个元素进行比较,交换一趟下来最大的元素一定在最右边。
2.特性说明
时间复杂度: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)
算法稳定性:稳定
3.算法实现
void BubbleSort(int array[], int size)
{
for (int i = 0; i < size - 1; ++i)
{
int cn = 0;
for (int j = 0; j < size - i - 1; ++j)
{
if (array[j]>array[j + 1])
{
Swap(&array[j], &array[j + 1]);
cn = 1;
}
}
if (!cn)
return;
}
}
4.快速排序
1.排序思想
在排序数组中选择一关键值key作为基准值,按照基准值对数组中元素进行划分为≤基准值和>基准值两部分的数据,同理,再对划分的两部分元素进行同样操作。
1.步骤:从排序数组中取一个基准值,按照基准值对数组下标区间(左闭右开)
中的元素进行划分。
2.目的:基准值左侧元素全<基准值,基准值右侧元素全>基准值。
3.递归以快排方式排基准值左侧
4.递归以快排方式排基准值右侧
2.特性说明
时间复杂度: O ( N ∗ l o g 2 N ) O(N*log_2N) O(N∗log2N)
空间复杂度: O ( l o g 2 N ) O(log_2N) O(log2N)
算法稳定性:不稳定
3.三种按照基准值划分的快速排序方式
基准值的选择:基准值可以选择任意的元素,一般情况下选择下标区间的最左侧元素或最右侧元素。区间为左闭右开。
- 方式一:hoare版本
时间复杂度: O ( n ) O(n) O(n)
- 方式二:挖坑法
时间复杂度: O ( n ) O(n) O(n)
- 方式三:前后指针
时间复杂度: O ( n ) O(n) O(n)
4.优化基准值
在前面我们都是直接为方便直接取区间初始值或是末尾值,而这大可能会取到极值,要么最大要么最小。
为避免选取到极值,我们可以在选取基准值时选取一个介于最小值和最大值间的数。
//三数取中法
int GetMiddleIndex(int array[], int left, int right)
{
int mid = left + ((right - left) >> 1);
//array[left]
//array[right-1]
//array[mid]
if (array[left] < array[right - 1])
{
if (array[mid] < array[left])
return left;
else if (array[mid]>array[right - 1])
return right - 1;
else
return mid;
}
else
{
if (array[mid] > array[left])
return left;
else if (array[mid] < array[right - 1])
return right - 1;
else
return mid;
}
}
5.快排再谈
(1)快速排序中,我们是采用递归方式进行数据的排序,而递归不断进行,区间元素不断减少,当元素减少到一定时,我们其实不需要再往下划分。当快排数据量很大的时候,递归深度会过深而造成栈的溢出,程序崩溃。当数据较少时,插入排序更适合。
可以设置一个阈值,当数据量小于所设阈值(来源:参考C++中STL的sort算法)时,递归结束,采用插入排序,从而提高快排效率。
快速排序有最优情况和最差情况。
最差情况:每次取的都是此待排序列中的最小元素(或最大元素),递归下去类似于一棵单支二叉树。
最优情况:每次取的基准值都将带排序元素对半分,递归下去展开类似一棵平衡二叉树。
最好的情况是,每次基准值都能将所有元素尽可能划分为数量差不多的两部分。
递归深度如何提前预知:最优情况平衡二叉树,最好情况时间复杂度, l o g 2 N log_2N log2N。
(2)若数据量较大,在未达到阈值之前,都通过快速排序递归来划分区间,而当通过递归划分时,数据量太大导致递归深度过深而造成栈溢出?
为不影响快排的时间复杂度,后序分组采用堆排序处理(因为堆排序时间复杂度也为
O
(
n
∗
l
o
g
2
n
)
O(n*log_2n)
O(n∗log2n))
6.算法实现
a.递归实现
//划分方式1:hoare版本
//时间复杂度:O(N)
int Partion1(int array[], int left, int right)
{
//三数取中法选取基准值
int keyIndex = GetMiddleIndex(array, left, right);
//将基准值和区间最后一个元素进行交换
Swap(&array[keyIndex], &array[right - 1]);
int begin = left;//注意:begin不能从0开始
int end = right - 1;
int key = array[end];
while (begin < end)
{
//begin从前往后找比基准值大的元素
while (begin < end && array[begin] <= key)
begin++;
//end从后往前找比基准值小的元素
while (begin < end && array[end] >= key)
end--;
if (begin < end)
{
Swap(&array[begin], &array[end]);
}
}
if (begin != right - 1)
{
Swap(&array[begin], &array[right - 1]);
}
return begin;
}
//划分方式2:挖坑法
//时间复杂度:O(N)
int Partion2(int array[], int left, int right)
{
int keyIndex = GetMiddleIndex(array, left, right);
Swap(&array[keyIndex], &array[right - 1]);
int begin = left;//注意:begin不能从0开始
int end = right - 1;
int key = array[end];
while (begin < end)
{
//初始end为坑,让begin从前往后找比基准值大的元素
while (begin < end && array[begin] <= key)
begin++;
//找到比基准值大的元素
if (begin < end)
{
//将begin位置元素覆盖end位置元素
array[end] = array[begin];
end--;
}
//begin成为新的坑位
//让end从后往前找比基准值小的元素去填begin位置的坑
while (begin < end && array[end] >= key)
end--;
if (begin < end)
{
//将end位置元素去填begin位置元素
array[begin] = array[end];
begin++;
}
//end位置又成为新的坑位
}
//最后用key值将最后一个坑位进行填充
array[begin] = key;
return begin;
}
//划分方式3:前后指针法
//时间复杂度:O(N)
int Partion3(int array[], int left, int right)
{
int cur = left;
int prev = cur - 1;
int keyIndex = GetMiddleIndex(array, left, right);
Swap(&array[keyIndex], &array[right - 1]);
int key = array[right - 1];
while (cur < right)
{
//cur从前往后找比基准值小的元素
//找到后,若prev的下一个位置和cur不等则进行交换
if (array[cur] < key && ++prev != cur)
{
Swap(&array[prev], &array[cur]);
}
++cur;
}
if (++prev != right - 1)
{
Swap(&array[prev], &array[right - 1]);
}
return prev;
}
//快速排序(递归实现)
void QuickSort(int array[], int left, int right)
{
if (right - left <16 || right-left==16)
{
InsertSort(array + left, right - left);
}
else
{
//1.在区间[left,right)找一基准值,按照基准值将区间划分为两部分
int div = Partion1(array, left, right);
//div基准值在数组中的下标
//2.递归排序基准值左侧
QuickSort(array, left, div);
//3.递归排序基准值右侧
QuickSort(array, div + 1, right);
}
}
b.非递归实现(利用栈实现)
//快速排序(非递归实现)
void QuickSortNor(int array[], int size)
{
Stack s;
StackInit(&s);
StackPush(&s, size);
StackPush(&s, 0);
while (!StackEmpty(&s))
{
//获取区间左边界
int left = StackTop(&s);
StackPop(&s);
//获取区间右边界
int right = StackTop(&s);
StackPop(&s);
//对区间[left,right)进行区间划分
if (right - left <= 1)
continue;
int div = Partion3(array, left, right);
//基准值左侧[left,div)
//基准值右侧[div+1,right)
//将基准值右侧压栈,先压右侧再压左侧
StackPush(&s, right);
StackPush(&s, div+1);
//将基准值左侧压栈,先压右侧再压左侧
StackPush(&s, div);
StackPush(&s, left);
}
StackDestroy(&s);
}
6.归并排序
1.排序思想
步骤
● 将区间中元素分为左右均等两部分[left,mid) [mid,right)
● 递归将左右两部分排序好
● 左右两侧两个有序序列合并成一个
思想同将两有序数组合并,将两有序链表合并。需借助辅助空间。
而我们可以看到,归并排序的区间划分为对半划分,其递归的状态类似于一棵平衡二叉树,其递归深度为平衡二叉树的高度。
故其时间复杂度并无最优最差情况的划分。
2.特性说明
时间复杂度: O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) 归并递归图划分好以后,一定是二叉平衡树的结构。
平衡二叉树高度: l o g 2 n log_2n log2n
空间复杂度: O ( n ) O(n) O(n)
在整个排序过程中需要借助n个元素的辅助空间O(n)+整个递归过程的空间复杂度O(logn),多个阶项取最高阶项。
算法稳定性:稳定
3.算法实现
a.递归实现
//将两有序区间进行合并
void MergeData(int array[], int left, int mid,int right, int* temp)
{
//左半侧
int begin1 = left;
int end1 = mid;
//右半侧
int begin2 = mid;
int end2 = right;
int index = left;
//将两区间中元素从前往后进行比较,将较小元素往temp中搬移
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
temp[index++] = array[begin1++];
else
temp[index++] = array[begin2++];
}
//将较长区间的剩余数据往temp中进行搬移
while (begin1<end1)
temp[index++] = array[begin1++];
while (begin2<end2)
temp[index++] = array[begin2++];
}
void _MergeSort(int array[], int left, int right, int* temp)
{
if (right - left <= 1)
{
return;
}
//1.将区间分为两部分
int mid = left + ((right - left) >> 1);
//2.递归排序左半侧[left,mid)
_MergeSort(array, left, mid, temp);
//3.递归排序右半侧[mid+1,right)
_MergeSort(array, mid , right, temp);
//4.将排序好的左半侧和右半侧进行合并
MergeData(array, left, mid, right, temp);
memcpy(array + left, temp + left, (right - left)*sizeof(int));
}
//归并排序(递归实现)
void MergeSort(int array[], int size)
{
int* temp = (int*)malloc(sizeof(int)*size);
if (temp == NULL)
{
assert(0);
return;
}
_MergeSort(array, 0, size, temp);
free(temp);
}
b.非递归-循环方式实现
//归并排序(非递归实现)
void MergeSortNor(int array[], int size)
{
int* temp = (int*)malloc(sizeof(int)*size);
if (temp == NULL)
{
assert(0);
return;
}
int gap = 1;
while (gap < size)
{
for (int i = 0; i < size; i += 2 * gap)
{
//每个区间都有gap个元素[left,mid) [mid,right)
int left = i;
int mid = left + gap;
int right = mid + gap;
if (mid>size)
mid = size;
if (right > size)
right = size;
//将两区间进行归并
MergeData(array, left, mid, right, temp);
}
memcpy(array, temp, sizeof(int)*size);
gap <<= 1;
}
free(temp);
}
6.计数排序
1.排序思想
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
2.特性说明
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度: O ( M A X ( N , 范围 ) ) O(MAX(N,范围)) O(MAX(N,范围)) 范围指区间范围
3. 空间复杂度: O ( 范围 ) O(范围) O(范围)
4. 稳定性:稳定
3.算法实现
void CountSort(int array[], int size)
{
//1.需统计区间中数据的范围,若事先知道数据的范围则不需要统计数据的区间范围
int minValue = array[0];
int maxValue = array[0];
for (int i = 0; i < size; ++i)
{
if (array[i] < minValue)
minValue = array[i];
if (array[i]>maxValue)
maxValue = array[i];
}
//2.计算需要多少个保存计数的空间
int range = maxValue - minValue + 1;
int* countArray = (int*)calloc(range, sizeof(int));
//3.统计每个元素出现的次数
for (int i = 0; i < size; ++i)
{
countArray[array[i] - minValue]++;
}
//4.按照统计的结果对数据进行回收
int index = 0;
for (int i = 0; i < range; ++i)
{
while (countArray[i]>0)
{
array[index] = i + minValue;
countArray[i] --;
index++;
}
}
free(countArray);
}
二、代码测试及结果
1.测试
void TestSort()
{
int arr[] = { 3, 1, 8, 6, 0, 2, 7, 9, 4, 5 };
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
InsertSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("插入排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
ShellSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("希尔排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
SelectSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("选择排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
SelectSortOP(arr, sizeof(arr) / sizeof(arr[0]));
printf("优化选择排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("选择排序再优化->堆排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
BubbleSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("冒泡排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
printf("hoare版本快速排序(递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
printf("hoare版本快速排序(非递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
MergeSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("归并(递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
MergeSortNor(arr, sizeof(arr) / sizeof(arr[0]));
printf("归并(非递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
CountSort(arr, sizeof(arr) / sizeof(arr[0]));
printf("计数排序为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
/*QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
printf("挖坑法快速排序(递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));*/
/*QuickSort(arr, 0, sizeof(arr) / sizeof(arr[0]));
printf("前后指针法快速排序(递归)为:\n");
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));*/
}
3.结果
三、总结
完整代码可点击进入仓库查看:https://gitee.com/confused-cat/code_-connection_point/tree/master/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95