用C语言实现的数据结构算法,下面来一个一个讲解:
(Swap函数在末尾,一个换位函数,理解即可)
1,插入排序
顾名思义就是一个值从前面开始一个一个插入,插入的时候排序一次,有 n 个数就排序 n 次
思想简单所以不介绍,代码如下:
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 (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,希尔排序
详细了解可以看我的前一篇文章,希尔排序就是相隔 gap 个距离的数值做排序,使其越来越接近有序,最后以间隔为 1 的 gap 小幅度排序结束。
代码如下:
void ShellSort(int* a, int n)
{
int gap = n / 3 + 1; //根据数组长度分大小
while (gap > 1) //最后一次跳出
{
for (int i = 0; i < n - gap; i++) //用i++可以巧妙地运用多组
{
int end = i;
int tmp = a[end + gap]; //插入的数值,间隔gap个
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap; //往前比较
}
else
break;
}
//当它最小或者遇到比它大的值时,赋值当前位置
a[end + gap] = tmp;
}
gap = gap / 3 + 1; //逐渐收缩
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\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 + gap] = a[end];
end -= 1; //往前比较
}
else
break;
}
//当它最小或者遇到比它大的值时,赋值当前位置
a[end + 1] = tmp;
}
}
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就可以达到优化的效果
3. 时间复杂度: O(N^1.3—N^2),不好计算,需要推导
4. 空间复杂度:0(1)
5. 稳定性:不稳定
3,选择排序
选择排序的思想是:
首先遍历数组,找出最大值和最小值,分别赋予给头和尾,随后头++,尾--,再次遍历数组,换头换尾,遍历,换头换尾,直到头尾指针相遇。
代码如下:
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int minsort = left, maxsort = left;
for (int i = left; i <= right; i++)
{
if (a[i] > a[maxsort])
{
maxsort = i;
}
else if (a[i] < a[minsort])
{
minsort = i;
}
}
Swap(&a[left], &a[minsort]);
if (maxsort == left) //判断避免max赋予了min的值
{
maxsort = minsort;
}
Swap(&a[right], &a[maxsort]);
left++;
right--;
}
}
直接选择排序的特性总结:
1. 选择排序非常好理解,但是效率不是很好,因为要一直遍历
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定,选择排序其实是不稳定的
4,堆排序
大小堆排序也很好理解,升序建大堆,降序建小堆,只要不破坏其父子关系,把头和尾交换后--n即可
代码如下:
//堆排序-子
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child < n - 1 && 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)
{
for (int i = (n - 1 - 1)/2; i >= 0; i--)
{
//建堆
AdjustDown(a, n, i);
}
//已经建好
while (n > 0)
{
Swap(&a[0], &a[n - 1]);
AdjustDown(a, n - 1, 0);
n--;
}
}
直接选择排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定
5,冒泡排序
冒泡排序的思想是:
从头到尾每一位数都来一次遍历,把最大值(最小值)放在最后,随后隐藏掉即可。
冒泡排序是一种运气排序,如果后面是有序,则不用继续遍历下去,如果是无序的 ,则需要一个一个来,比插入排序的时间复杂度还久,选用原因是:代码简单啊
代码如下:
//冒泡排序
void Bubble(int* a, int n)
{
for (int i = n - 1; i >= 0; i--)
{
int count = 0;
for (int j = 0; j < i; j++)
{
if (a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
count = 1;
}
}
if (count == 0)
break;
}
}
冒泡排序的特性总结:
1. 容易理解
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
6,快速排序
本次快排有3种,都是常用到的:
1. hoare版本 2. 挖坑法 3. 前后指针版本
第一种:hoare快排
hoare的思想:
首先定义 left 和 right 、key 指针,left 和 right 分别表示左和右,key 代表比较值,关键点:如果选 left 为 key,就要让 right 指针先走,如果选 right 作为 key ,反之。
right 先走,走到比 key 小的位置停下,到 left 走,走到比 key 大的位置停下,然后 a[left] 与 a[right] 互换,right 继续走,重复,直到 right 与 left 相交的时候,把相交的位置 meet 与 key 所在的值交换,给到的 meet 值可以作为递归的 left 和 right。
代码如下:
int QuickHoare(int* a, int left, int right)
{
//三数取中法
int mid = GetKey(a, left, right);
Swap(&a[left], &a[mid]);
int key = left;
while (left < right)
{
//右边先移
while (left < right && a[right] >= a[key])
{
right--;
}
//再到左边移动
while (left < right && a[left] <= a[key])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
//给到中间来
int meet = left;
return meet;
}
递归的函数一齐放到快排的最后。
第二种:挖坑法
挖坑法的思想:
left right hole 指针,hole作为坑,首先取出 left 的值赋予 hole,随后 right 先走,到比 hole 大的位置停下,a[right] 直接 赋予 a[left] 上,因为一开始 left 所在的值给了 hole ,所以不用担心被覆盖。随后 left++,遇到比 hole 小的就停下,a[left] 赋予 a[right] 上,随后right--,赋值,left++,赋值,知道两指针相交的位置 meet ,把 hole 的值给到 meet 上。
代码如下:
int QuickDig(int* a, int left, int right)
{
//挖坑
int hole = a[left];
//排序
while (left < right)
{
//先走右
while (left < right && a[right] >= hole)
right--;
a[left] = a[right]; //填坑
while (left < right && a[left] <= hole)
left++;
a[right] = a[left]; //填坑
}
a[left] = hole;
int meet = left;
return meet;
}
第三种:前后指针法
前后指针法的思想:
给一个 key prev cur 指针,cur = prev + 1 ,key 存放 prev 所在的值,如果 cur 的值小于 key 的值,则 cur 与 prev 的+1互换,如果 cur 遇到大于 key 的值,则一直++,直到 cur = right,随后 key 与 prev 互换,prev 作为 meet 递归下去。
代码如下:
int QuickPoint(int* a, int left, int right)
{
int prev = left, cur = prev + 1;
//三数取中
//int mid = GetKey(a, left, right);
//Swap(&a[mid], &a[prev]);
int key = prev;
while (cur <= right)
{
if (a[key] > a[cur] && ++prev != cur) //避免原地TP
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[key], &a[prev]);
return prev;
}
优化代码 -- 三数取中法的代码:
int GetKey(int* a, int left, int right)
{
int mid = (left + right) >> 1; // 两值/2
if (a[left] < a[mid]) // 看 left mid right 哪个在中间
{
//left < mid ? right
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right; // left < right < mid
}
else // (a[left] > a[mid])
{
//mid < left ? right
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
递归的代码:
如果需要优化,则可以使用 三数取中法 和 分治递归
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = QuickPoint(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
1、如果这个子区间是数据较多,继续选key单趟,分割子区间分治递归
2、如果这个子区间是数据较小,再去分治递归不太划算
//if (end - begin > 20)
//{
// int keyi = QuickPoint(a, begin, end);
// // [begin, keyi-1] keyi [keyi+1, end]
// QuickSort(a, begin, keyi - 1);
// QuickSort(a, keyi + 1, end);
//}
//else
//{
// //HeapSort(a + begin, end - begin + 1);
// InsertSort(a + begin, end - begin + 1);
//}
}
快速排序的特性总结:
1. 你可以永远相信快排,快排yyds!
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
7,非递归快排
非递归快排需要用到栈,利用栈的原理,类似于递归,假如区间是0-10,第一次放进0,10;第二次放0,5,6,10;第三次放0,5,6,8,9,10。。。每一次取 meet 值,都已经排好序了,随后[6,10]已经排好,开始[0,5]的区间,先放0,2,3,5;再到0,2,3,4,5,随后取出4,5作为左右区间排序,直到排好,这里我们需要 malloc 一个空间存放排序后的值,最后再复制给原数组,free掉。
代码如下:
void QuickSortNonR(int* a, int left, int right)
{
stack st;
StackInit(&st);
//先存左右区间
StackPush(&st, left);
StackPush(&st, right);
//空了就代表排序完成,没空继续
while (!StackEmpty(&st))
{
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
int key = QuickHoare(a, begin, end);
if (begin < key - 1)
{
StackPush(&st, begin);
StackPush(&st, key - 1);
}
if (end > key + 1)
{
StackPush(&st, key + 1);
StackPush(&st, end);
}
}
StackDistroy(&st);
}
8,归并排序
归并排序的思想:
归并排序实质上是一个一直分治的过程,它把原数组拆分成小份比较排序,这样使得,每一组比较的数组都是有序的,只需要创建两个指针,left 代表第一组的头,right 代表第二组的头,创建一个空间,谁小就先放进去,如果哪个数组先结束,另外一组就直接加进队尾,因为是有序的,最后复制到原数组上去,free掉。
代码入下:
//归并操作
void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int i = begin1, j = i;
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++];
}
for (; j <= end2; j++)
{
a[j] = tmp[j];
}
}
//子函数
void _MergeSort(int* a,int *tmp, int left, int right)
{
if (left >= right)
return;
int mid = (left + right) >> 1;
_MergeSort(a, tmp, left, mid);
_MergeSort(a, tmp, mid + 1, right);
int begin1 = left, begin2 = mid + 1, end1 = mid, end2 = right;
_Merge(a, tmp, begin1, end1, begin2, end2);
}
//归并排序
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc false");
return;
}
int left = 0; int right = n - 1;
_MergeSort(a, tmp, left, right);
free(tmp);
}
归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
9,非递归归并排序
非递归归并的话要求就多了,先看代码:
//归并操作
void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int i = begin1, j = i;
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++];
}
for (; j <= end2; j++)
{
a[j] = tmp[j];
}
}
//非递归归并排序
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc false");
return;
}
int left = 0, right = n - 1;
int gap = 1; //间隔为 1 ,再往后递增
while (gap < n)
{
for (int j = 0; j < right; j += gap*2)
{
//[j, j+gap-1] [j+gap, j+gap*2-1]
int begin1 = j, begin2 = j + gap, end1 = j + gap - 1, end2 = j + gap * 2 - 1;
//考虑多种情况
//1,前区间结尾小于right 和 只有前区间,没到后区间
if (end1 >= right)
break;
//2,有前区间,但后区间小于right的
if (end2 > right)
{
end2 = right;
}
_Merge(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2;
}
}
这里复杂的是 _Merge函数 前后两个区间的取值,我们需要注意不可以让 end1 或者 end2 越界,间距gap 每次 *=2,令 j = gap*2,考虑3种状态,有可能到一半就到头了,后半身还没进去,图中1和3条件可以一样,因为都没有后区间,干脆不执行。
10,计数排序
计数排序其实很简单,它涉及到一个映射问题
计数排序的思想:
创建一个数组,该数组的下标对应的是要排序数组的值,比如上图,data 中 4就放在下标为 count 中4的下标,这时候该下标+1,如果有3个6,count 的[6]就等于3,代表6有3个。同样的道理,1就放1,2就放2,放一个就+1个,最后按照下标 count[6] = 3的值打印3个6。
由于是绝对映射,所以可能存在空间浪费,比如 data 最小值为1000,最大值为1001,难道要创建[0,1001]吗?!所以这里需要用到相对映射,先遍历一遍数组,找到最大值和最小值,最小值就是映射的值,后面的加减只需要 +- 最小值min即可。
代码如下:
void CountSort(int* a, int n)
{
int gap = 0, min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
//求出最大值和最小值的差,相对映射
gap = max - min + 1;
int* count = (int*)malloc(sizeof(int) * gap);
memset(count, 0 ,sizeof(int) * gap);
//标记到count上
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//赋值回去
int i = 0;
for (int j = 0; j < gap; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
4. 稳定性:稳定
总结: