目录
一、插入排序
直接插入排序
图解:
代码实现:
希尔排序
单趟排序图解:
代码实现:
二、选择排序
直接选择排序
图解:
代码实现
堆排序
具体思想:
图解编辑
代码实现
三、交换排序
具体思想:
冒泡排序
具体思想:
动图图解如下:
代码实现
快速排序
Hoare版本
静态图解
编辑
动态图解:编辑
代码实现
前后指针法
静态图解
动态图解编辑
代码实现
挖坑法
静态图解
动态图解编辑
代码实现
三数取中
代码实现
快速排序的非递归实现
代码实现
快排总结
代码如下
四、归并排序
具体思想
静态图解
编辑
动态图解
代码实现
五、计数排序
具体思想
静态图解
代码实现
稳定性 时间复杂度 空间复杂度
具体思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
就比如我们玩的扑克牌用的最多的排序方式就是插入排序的思想。
具体思想:当我们需要插入一个数据时,前边的其他数据已经有序,那么就拿着这个数据从最后一个进行比较,然后插入到合适的位置上。
//直接插入排序
void InsertSort(int* a, int n)
{
int end = 0, x = 0;
for (int i = 0; i < n - 1; i++)
{
end = i;
x = a[end + 1];
while (end >= 0)//只有当end=0时走到第一个数
{
if (a[end] > x)
{
a[end + 1] = a[end];
end--;
}
else//若a[end]<=x那就直接跳出循环
{
break;
}
}
a[end + 1] = x;
}
}
希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序,因D.L.Shell于1959年提出而得名。
虽说是希尔排序,其实用到的思想跟直接插入排序一样,只不过希尔大佬把直接插入排序进行了优化,直接插入排序在对已经有序或者是接近有序的数据排序时效率会很高,希尔大佬就在想,是不是可以先把数据搞得有序,然后再对其进行直接插入排序。
具体思想:希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成k个组,所有距离为k的记录分在同一组内,并对每一组内的记录进行排序。然后,将k--,重复上述分组和排序的工作。当到达k=1时,所有记录在统一组内排好序。
由此可见,当gap不为1时可以看做是对整体数据进行优化,使其更好的进行直接插入排序。
//希尔排序
void ShellSort(int* a, int n)
{
//分组排
int gap = 3;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int j = 0; j < gap; j++)
{
for (int i = 0; i < n - gap; i += gap)
{
int end = i;
int x = a[end + gap];
while (end >= 0)
{
if (a[end] > x)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = x;
}
}
}
多组一锅炖
//int gap = 3;
//while (gap > 1)
//{
// gap = gap / 3 + 1;
// assert(a);
// for (int i = 0; i < n - gap; i++)
// {
// int end = i;
// int x = a[end + gap];
// while (end >= 0)
// {
// if (a[end] > x)
// {
// a[end + gap] = a[end];
// end -= gap;
// }
// else
// {
// break;
// }
// }
// a[end + gap] = x;
// }
//}
//
}
具体思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接插入排序的思想其实就是选择排序,但是我们可以将其优化,既然每一次遍历能选择一个最大的或者是最小的,那么我们是不是可以一次遍历就选出最大的或者最小的呢?
那么接下来我们就每次遍历选出最大的和最小的,最大的和末尾值进行交换,最小的和第一个值进行交换,然后对除了首位和末尾的其他数据进行再次直接选择排序。
//选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int min = begin, max = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] < a[min])
{
min = i;
}
if (a[i] > a[max])
{
max = i;
}
}
Swap(&a[begin], &a[min]);
if (max == begin)
{
max = min;
}
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。如果看完这段话还是有点儿不理解的话我建议去看一下我之前的博客。
这里需要引入一个向下调整算法,如果这里的向下调整算法不知道的,我建议看一下我之前的堆排序的一篇文章,那里面有所提到。
//堆排序
void HeapSort(int* a, int 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--;
}
}
//向下调整算法
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root)
{
int parent = root;
int child = 2 * parent + 1;
while (child < n)
{
//小根堆
//由于是小根堆 所以找出左右孩子小的那个孩子
if ((a[child + 1] > a[child]) && (child + 1) < n)
{
child++;
}
//小根堆
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排
序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序我认为是八大排序中最容易理解的排序,具体思想是从第一个数进行遍历直到最后一个数,将他们的最大值遍历出来,然后放在末尾,随后对剩下的数据再次进行如上操作。
// 冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = n; i > 1; i--)//循环次数为n-1是因为 最后一个数不用冒泡就是有序的了
{
for (int j = 0; j < i - 1; j++)
//循环次数最高为n-1是因为 j控制的是需要交换的两个数中的左边的那个数的下标
//所以只需要到n-1 就行了 因为这样的话 右边的数的下标就到了 n
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
/*for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}*/
}
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
//三数取中
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
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[left], &a[keyi]);
return left;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int mini = GetMidIndex(a, left, right);
Swap(&a[mini], &a[left]);
int key = a[left];
int pivot = left;
while (left < right)
{
//右边找小,扔到左边的坑位
while (left < right && a[right] >= key)
right--;
a[pivot] = a[right];
pivot = right;//自身形成新的坑位
//左边找大,扔到右边的坑位
while (left < right && a[left] <= key)
left++;
a[pivot] = a[left];
pivot = left;//自己形成新的坑位
}
a[pivot] = key;
return pivot;
}
三数取中的引入,主要是为了解决排序接近有序或者有序的情况下快速排序的效率将会非常的低,并且递归的深度非常的深导致的问题。
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
return mid;
else if (a[left] > a[right])
return left;
else
return right;
}
else//a[left]>a[mid]
{
if (a[mid] > a[right])
return mid;
else if (a[left] < a[right])
return left;
else
return right;
}
}
由于快排的递归调用可能因为数据有序而过多的递归调用,这样的话将会增加栈的开销。堆的空间相较于栈来说就大的多了,所以我们可以学习一下快排的非递归调用,具体图解如下:
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
//先入0-9区间
StackPush(&st, left);
StackPush(&st, right);
while (!StackEmpty(&st))
{
//取出0-9区间
int end = StackTop(&st);
StackPop(&st);
int begin = StackTop(&st);
StackPop(&st);
//找中间基准值、划分左右区间
int keyi = PartSort3(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end]
//先入右区间
if (keyi + 1 < end)
{
StackPush(&st, keyi + 1);
StackPush(&st, end);
}
//再入左区间
if (begin < keyi - 1)
{
StackPush(&st, begin);
StackPush(&st, keyi - 1);
}
}
StackDestroy(&st);
}
以上我们用图解陈述了3种方法的具体过程,那么既然要实现真正的快排,肯定要有个快排函数来进行递归调用。
递归版本
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
//小区间优化,当分割到小区间时,不在用递归思路让这段子区间有序
//对于递归快排,减少递归次数
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort3(a, left, right);//前后指针法
//区间划分[begin , keyi-1] keyi [keyi+1 , end]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
非递归版本
void QuickSortNonR(int* a, int left, int right)
{
if (left >= right)
return;
//小区间优化,当分割到小区间时,不在用递归思路让这段子区间有序
//对于递归快排,减少递归次数
if (right - left + 1 < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
int keyi = PartSort3(a, left, right);//前后指针法
//区间划分[begin , keyi-1] keyi [keyi+1 , end]
QuickSortNonR(a, left, keyi - 1);
QuickSortNonR(a, keyi + 1, right);
}
}
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
// 归并排序递归实现
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) / 2;
// [left, mid] [mid+1, right] 有序
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int i = left;
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++];
}
// tmp 数组拷贝回a
for (int j = left; j <= right; ++j)
{
a[j] = tmp[j];
}
}
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);
tmp = NULL;
}
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(MAX(N,范围))
3. 空间复杂度:O(范围)
// 计数排序
//计数排序
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
//找出最大值和最小值
for (int i = 1; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;//计算范围
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range);//将count数组元素全部置为0
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//统计次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
//根据次数,进行排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
}