test.c
#include"Sort.h"
int main()
{
TestSort();
return 0;
}
sort.h
#pragma once
#include"stdio.h"
//比较类排序:
//插入排序
void InsertSort(int array[], int size);
//希尔排序
void ShellSort(int array[], int size);
//选择排序
void SelectSort(int array[], int size);
//选择排序
void SelectSortOP(int array[], int size);
//堆排序
void HeapSort(int array[], int size);
//冒泡排序
void BubbleSort(int array[], int size);
//改良版本
void BubbleSortOP(int array[], int size);
//快速排序
void QuickSort(int array[], int left, int right);
//将递归转换为循环
void QuickSortNor(int array[], int size);
//归并排序
void MergeSort(int array[], int size);
//将递归转换为循环
void MergeSortNor(int array[], int size);
//非比较类排序
void CountSort(int array[], int size);
void TestSort();//测试
sort.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "Sort.h"
#include <stdio.h>
#include "stack.h"
#include<malloc.h>
#include<string.h>
void PrintArray(int array[], int size)//打印数组
{
for (int i = 0; i < size; ++i)
printf("%d ", array[i]);
printf("\n");
}
//插入排序(从小到大)
//每次读取数据时,我们将其进行插入:
//1.找待插入数据的位置
//2.插入
//在整个排序过程中,没有使用额外的辅助空间,空间复杂度:O(1)
void InsertSort(int array[], int size)
{
//注意:i从1开始
for (int i = 1; i < size; ++i)//外层循环控制:有size-1个数据需要插入
{
//模拟单个元素插入过程
int key = array[i];
int end = i - 1;
//找待插入元素在区间中的位置
//最差情况下:需要i位置之前所有已插入的元素整体往后进行搬移---O(n)
while (end >= 0 && key < array[end])// key < array[end] 这句中 没有=:算法稳定 有=:算法不稳定
{
array[end + 1] = array[end];
end--;
}
//插入元素
//注意:当插入0元素时,end+1 = -1+1 = 0;
array[end + 1] = key;
}
}
//问题?什么情况下插入排序性能最差 (时间复杂度问题)
//给的数据为顺序,让你排列成降序--最差 时间复杂度:O(N^2)
//反之给的数据为顺序,让你排列成顺序--最好 时间复杂度:O(N)
//即:插入排序使用的场景:
//1.数据有序或者接近有序(注意:有序或接近有序与用户所需要序列一致)
//2.数据量少--搬移元素个数少
//问题:现实中数据量往往比较大,数据比较乱--要求:利用插入排序的思想来排序?
//解:将用户所给的场景向插入排序的场景进行转变
//数据凌乱--想办法将该数据逐渐接近有序--比较困难
//数据量大--想办法让数据逐渐变小--相对简单--将数组进行分组--最常见的(对数据进行平均分组)
//对数据进行平均分组:每组数据个数较少,用插入排序对每个分组进行排序
//接近有序:小的数据尽量靠前,大的数据尽量靠后,不大不小的数据尽量靠中间
//分组方式:让数据按照一定的间隔进行分组
//0.间隔(未知)个位置的所有数据进行排序
//。。。。。。
//1.间隔三个位置的所有数据进行排序
//2.间隔两个位置的所有数据进行排序
//3.间隔一个位置的所有数据进行排序
//---希尔排序(shellsort)
//希尔排序
void ShellSort(int array[], int size)
{
int gap = size;
while (gap > 1)
{
//gap/=2//希尔给出的
//gap每次取素数
gap = gap / 3 + 1;//kunth大量实验证明此为最好
//注意:i从1开始
for (int i = gap; i < size; ++i)//外层循环控制:有size-1个数据需要插入
{
//模拟单个元素插入过程
int key = array[i];
int end = i - gap;
//找待插入元素在区间中的位置
//最差情况下:需要i位置之前所有已插入的元素整体往后进行搬移---O(n)
while (end >= 0 && key < array[end])// key < array[end] 这句中 没有=:算法稳定 有=:算法不稳定
{
array[end + gap] = array[end];
end -= gap;
}
//插入元素:插入到当前分组的下一个位置
array[end + gap] = key;
}
}
}
//看排序算法是否稳定:
//1.排序算法原理在插入时是否隔着区间(隔着元素)来进行插入
//2.排序算法原理在交换时是否隔着区间(隔着元素)来进行交换
//是:则不稳定(相等的元素则会跳过去)
//否:则稳定
//----》希尔排序不稳定
//空间复杂度:O(1)
//时间复杂度:O(N^1.25)~O(1.6N^1.25)
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
//选择排序
//时间复杂度:O(N^2)
//空间复杂度:O(1)
//稳定性:不稳定
void SelectSort(int array[], int size)
{
for (int i = 0; i < size - 1; ++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]);
}
}
}
//选择排序
void SelectSortOP(int array[], int size)
{
int begin = 0, end = size - 1;
while (begin < end)
{
//在[begin,end]区间中找最大和最小的元素
int maxPos = begin,minPos = begin;
int j = begin + 1;
while (j <= end)
{
if (array[j] >array[maxPos])
maxPos = j;
if (array[j] < array[minPos])
minPos = j;
++j;
}
//如果最大元素不在区间最后的位置、
if (maxPos != end)
Swap(&array[maxPos], &array[end]);
//如果end位置存储的刚好是最小的元素,上面的交换就将最小的元素位置更改了--maxpos
//最小元素的位置发生了改变,则必须要更新minPos
if (minPos == end)//minPos和end刚好相等的情况下,说明在交换之前最小元素一定在最后一层
minPos = maxPos;
//最小的元素不在区间的起始位置
if (minPos != begin)
Swap(&array[minPos], &array[begin]);
++begin;
--end;
}
}
//选择排序有什么缺陷?
//重复遍历太多
//怎么才能减少重复遍历次数?
//---堆排序
//1.建堆:建大堆还是建小堆? 取决于用户需要升序(大堆)还是降序(降序)
//问题:建堆需要用到什么调整?---向下调整
//______向下调整是从哪个位置开始调整的?倒数第一个非叶子节点的位置~到根节点所有的节点应用向下调整即可建成堆
//______倒数第一个非叶子节点是怎样算的?倒数第一个非叶子节点就是最后一个叶子节点的双亲:(size-1-1)/2
//2.利用堆删除的思想进行排序
//______将堆顶元素与队中最后一个元素进行交换(即将数组中最大或最小的元素放到末尾)
//______将堆中有效元素个数减少一个
//______将堆顶的元素向下调整
//核心:向下调整
void HeapAdjust(int array[], int size, int parent)
{
//用child标记parent较大的孩子,默认先标记parent的左孩子
//先标记左孩子的原因是:如果parent有孩子,parent一定是先有左孩子,然后才有右孩子
int child = parent * 2 + 1;
while (child < size)
{
//找parent中较大的孩子:parent左右孩子比较
//必须先保证parent的右孩子存在
if (child+1 < size && array[child + 1] > array[child])
child += 1;
//检查parent是否满足堆的性质
if (array[child] > array[parent])
{
Swap(&array[child], &array[parent]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
//堆排序
//空间复杂度:O(1)
//时间复杂度:O(Nlog2^N)
//稳定性:不稳定
void HeapSort(int array[], int size)
{
int end = size - 1;
//1.建堆
//注意从倒数第一个非叶子节点的位置开始使用堆调整,一直调整到根节点的位置
for (int root = (size - 2) / 2; root >= 0; root--)
HeapAdjust(array, size, root);//向下调整
//2.排序-->利用堆删除的思想进行排序
while (end)
{
Swap(&array[0], &array[end]);
HeapAdjust(array, end, 0);
end--;
}
}
//交换排序:
//1.冒泡排序
//for()--》外层循环:控制冒泡的趟数
// for()--》内层循环:控制冒泡的方式--用相邻的两个元素来进行比较,不满则要求将相邻的元素进行交换,直到区间末尾
//2.快排
//冒泡排序
//时间复杂度:O(N^2)
//空间复杂度:O(1 )
//稳定性:稳定
//void BubbleSort(int array[], int size)
//{
// //控制冒泡的趟数
// for (int i = 0; i < size - 1; ++i)//-1的目的是可以少冒一趟,因为最后一次冒泡区间中只剩余一个元素
// {
// //具体冒泡的方式:用相邻位置的元素进行比较,如果不满足条件,就进行交换
// /*方法一:
// //j:表示前一个元素的下标
// for (int j = 0; j < size - i - 1; ++j)//-1目的:j最多只能取到冒泡区间的倒数第二个元素
// {
// if (array[j] > array[j + 1])
// Swap(&array[j], &array[j + 1]);
// }
// */
// //方法二:方法二为方法一变换而来,注意方法代码第三行j-1和j的关系(防止写成降序)
// //j:表示后一个元素的下标
// for (int j = 1; j < size - i; ++j)//-1目的:j最多只能取到冒泡区间的倒数第二个元素
// {
// if (array[j-1] > array[j])
// Swap(&array[j], &array[j - 1]);
// }
//
// }
//}
//问题:给出的序列为0123456798,这个序列在第一躺中便已经排好,如果按照上述方法,则会浪费大量时间
//void BubbleSortOP(int array[], int size)
//{
// int flag = 0;
// //控制冒泡的趟数
// for (int i = 0; i < size - 1; ++i)//-1的目的是可以少冒一趟,因为最后一次冒泡区间中只剩余一个元素
// {
// //具体冒泡的方式:用相邻位置的元素进行比较,如果不满足条件,就进行交换
// //j:表示后一个元素的下标
// flag = 0;//该躺冒泡还没有比较,因此将flag设置为0
// for (int j = 1; j < size - i; ++j)//-1目的:j最多只能取到冒泡区间的倒数第二个元素
// {
// if (array[j - 1] > array[j])
// {
// Swap(&array[j], &array[j - 1]);
// flag = 1;//在该躺冒泡时区间还无序
// }
// }
// if (!flag)
// return;
// }
//}
//三数取中法:三个数据取最中间的数据作为基准值
int GetMiddleIndex(int array[], int left, int right)
{
int mid = left + ((right - left) >> 1);
//三个数据:left,mid,right-1
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;
}
}
int Partion1(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int keyofindex = GetMiddleIndex(array, left, right);
int key;
if (keyofindex != right - 1)
{
Swap(&array[keyofindex], &array[right - 1]);
}
key = array[keyofindex];
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;
}
//如何进行区间的基准值分割?
//数据分割:
//方法一:(hore)
//先取基准值为最后一个元素
//只要begin和end没有遇到或者没有错过(begin和end区间中有元素)
//begin从前往后移动,找比基准值大的元素,找到之后停止
//end从后往前找,找比基准值小的元素,找到之后停止
//交换begin和end位置的元素--(交换时,是跨区间交换的,所以不稳定)
//当begin和end相遇时,交换最后一个元素和相遇位置的元素
//方法二:挖坑法
int Partion2(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int keyofindex = GetMiddleIndex(array, left, right);
int key;
if (keyofindex != right - 1)
Swap(&array[keyofindex], &array[right - 1]);
key = array[right - 1];
while (begin < end)
{
//end位置形成了一个新的坑
//让begin从前往后找比基准值大的元素
while (begin < end && array[begin] <= key)
begin++;
//让begin位置大的元素天end位置的坑
if (begin < end)
{
array[end] = array[begin];
end--;
}
//begin位置形成了一个新的坑
//让end从后往前找,找比基准值小的元素,填begin位置的坑
while (begin < end && array[end] >= key)
end--;
if (begin < end)
{
array[begin] = array[end];
begin++;
}
}
//用基准值填最后一个坑
array[begin] = key;
return begin;
}
//方法三:前后指针法
//1.设置两个指针cur和prev,cur在前,prev在后
//2.当array[cur]<key&&++prev!=cur时,同时往后移动一个位置,否则cur移动一个位置
//3.当2满足时交换cur和prev
//4.最后交换基准值和Prev位置的值
//如果prev和cur紧挨着,说明cur还没有遇到比基准值大的元素
//如果prev和cur有差距,说明cur在遍历过程中,已经遇到比基准值大的元素了
//而且prev和cur之间的元素都是比基准值大的元素
int Partion3(int array[], int left, int right)
{
int cur = left;
int prev = cur - 1;
int keyofindex = GetMiddleIndex(array, left, right);
int key;
if (keyofindex != right - 1)
{
Swap(&array[keyofindex], &array[right - 1]);
}
key = array[keyofindex];
while (cur < right)
{
if (array[cur] < key && ++prev != cur)
Swap(&array[cur], &array[prev]);
++cur;
}
if (++prev != right -1)
Swap(&array[right -1], &array[prev]);
return prev;
}
//快速排序
//[left,right)表示待排序元素的区间
//void QuickSort(int array[], int left, int right)
//{
// if (right - left > 1)
// {
// //Partion按照基准值(就是区间中的某个元素)对区间进行划分成两部分,左部分元素比基准值小,右部分比基准值大
// //该函数返回基准值在区间中的位置
// //[left,right)区间中的基准值位置已经存放好了,基准值的左侧和基准值的右侧不一定有序
// int div = Partion3(array, left, right);
//
// //基准值左侧[left,div)
// QuickSort(array, left, div);
//
// //基准值右侧[div+1,right)
// QuickSort(array, div + 1, right);
// }
//}
//优化上述代码:
void QuickSort(int array[], int left, int right)
{
if (right - left < 16)//为什么是16,经过大量实验表明16为插入排序的最合适数据量
{
//[left,right)区间中数据少到一定程度,使用插入排序来优化
InsertSort(array + left, right - left);
}
else
{
//Partion按照基准值(就是区间中的某个元素)对区间进行划分成两部分,左部分元素比基准值小,右部分比基准值大
//该函数返回基准值在区间中的位置
//[left,right)区间中的基准值位置已经存放好了,基准值的左侧和基准值的右侧不一定有序
int div = Partion3(array, left, right);
//基准值左侧[left,div)
QuickSort(array, left, div);
//基准值右侧[div+1,right)
QuickSort(array, div + 1, right);
}
}
//快排原理:
//先按照基准值对数据来进行划分:(达到什么效果?)按照基准值将区间中的数据划分为两部分,一部分比基准值小,一部分比基准值大---》partion(用此来进行分割)
//返回基准值所在的位置div--》[left,div)div[div+1,right)
//使用快速排序递归对基准值的左侧进行排序--[left,div)
//使用快速排序递归对基准值的右侧进行排序--[div+1,right)
//快速排序递归框架与二叉树的前序遍历程序框架类似
//划分方法:按照基准值进行划分--》
//基准值获取方法:理论上区间中任意一个值都可以作为基准值,简单起见一般选择区间最左侧或者最右侧数据为基准值
//按照基准值进行划分,划分方法: partion123
//问题:为什么取基准值时,尽量靠近中间值
//以中间值作为基准值进行划分,划分好之后基准值左右两侧的数据基本上相等
//基准值选择越大或者越小,划分好之后基准值一侧数据非常多,一侧数据非常少
//极端情况:假设每次进行划分时,取的基准值都是区间中最大值或者区间中的最小值(最值)
//按照基准值(最小值)进行划分:基准值左侧没有数据,数据全部集中到基准值的右侧
//快速排序后图解退化成单支树--》时间复杂度:O(N^2)
//如果每次取基准值时,基准值时中间值,此时快速排序图解将为平衡二叉树
//最佳情况下时间复杂度:取决于树的高度log2^N--》元素个数--》Nlog2^N
//时间复杂度看的时最差情况,是否说明快排时间复杂度O(N^2)?
//一般情况下认为快排的时间复杂度为:Nlog2^N
//前提是尽量避免取到极值的概率--以前取极值的方式:取区间最右侧的值作为极值---按照该种方式取基准值拿到极值的概率非常高的
//最好避免该种情况
//一般是采用三数取中法来避免的
//三数取中法:区间最左侧取一个数据,区间最右侧取一个数据,从区间最中间取一个数据,以三个数据中的中间值作为基准值
//按照三数取中法优化之后,在划分时取到极值的概率就降低了,因此说:快速排序平均时间复杂度为:Nlog2^N
//最终时间复杂度是按照平衡二叉树的图解来求的
//空间复杂度:递归函数空间复杂度求解方式--》每次递归的时间复杂度*递归的深度
//每次递归时并没有借助辅助中间--O(1)
//深度log2^N
//空间复杂度O(log2^N)
//问题:如果数据量特别大,递归的深度会非常深,递归深度达到一定程度会造成栈溢出
//原因:因为递归调用,最深层函数如果没有推出,上层的一些递归调用是不会结束的,而每次递归都是一次函数调用,每个函数调用都需要在栈上给函数划分一个栈帧(一段栈空间),函数递归调用深度越深,划分的栈空间就越多,而栈是有大小限制的,如果递归层次深到一定程度,栈空间不足时,就无法给该次递归函数划分栈空间导致递归失败,即栈溢出
//解决方法:
//1.让递归的深度不要那么深——随着我们在里面不断的进行划分,刚开始的数据非常大(就是有n个数据),递归图解中:越往下,每个分组中的数据量越少,(分组中只剩下一个数据时,就不会往下递归了),我们没有必要让区间中只有一个数据时再往回退,越往下数据量越少,当数据量少的时候,这样排序反而不好了(当数据量越少时,我们用插入排序效果会更好)
//2.不使用递归,使用循环----循环版本的快排
//将递归转换为循环--》有些算法可以直接转:比如Fac求阶乘--》因为问题就是一个迭代器问题
//有些算法不能直接转:借助stack实现
//为什么递归转循环可以使用栈:递归程序是后调用的先退出,与栈的特性完全符合
//用栈来将递归转化为循环
void QuickSortNor(int array[], int size)
{
int left = 0;//左边界
int right = size;//右边界
Stack s;
StackInit(&s);//对栈进行初始化
StackPush(&s, right);
StackPush(&s, left);
while (!StackEmpty(&s))
{
//先按照基准值来进行划分
left = StackTop(&s);
StackPop(&s);
right = StackTop(&s);
StackPop(&s);
if (right - left > 1)
{
int div = Partion2(array, left, right);
//排基准值的左半侧--将右半部分的区间入栈[div+1, right)
StackPush(&s, right);
StackPush(&s, div+1);
//排基准值的右半侧--将左半部分的区间入栈[left,div)
StackPush(&s, div);
StackPush(&s, left);
}
}
StackDestroy(&s);
}
//思考:(帮助复习)
//1.快排的原理
//2.快排代码框架搭建
//3.按照基准值划分区间的三种方式:hore,挖坑法,前后指针法
//4.对快排最优和最差情况的分析
//5.采用三数取中法对快排进行优化
//6.采用插入排序对快排进行优化
//7.时间复杂度,空间复杂度,稳定性分析
//8.将递归转换为循环
//9.快排的应用场景:数据量大数据越随机越好
//快排是目前为止速度最快,堆排序与快排的时间复杂度皆为O(Nlog2^N),但是实际情况下快排比堆排序速度快
//归并排序:
//与快速排序类似的地方是:都需要对区间来进行划分
//不同点:快排是按照基准值对区间进行划分的,存在极端情况,归并排序是每次都是对数据进行均分的,不存在最差的情况
//原理:每次对区间中的数据进行均分,将左半部分归并排序好,再将右半部分归并排序好,将左右两半部分归并成一个有序的序列
//本质:先逐次对区间来进行均分,均分到区间中只有一个元素时然后再向上进行归并
//归并:将两个有序的区间归并成一个有序的区间---给两个有序的数组,将两个有序的数组合并成一个有序的数组
//问题:4325,我们先将其分为4 3 2 5 四个数据,合并时34 25 ,再次合并时为什么可以直接将其合并为2345(按照什么原理?)
//答:此时我们有两个有序的数组,借助一段辅助空间,在辅助空间中,两个数组依次比较,然后插入数组
//注意:
//快速排序中我们会寻找基准值,然后将数据分为两部分,此时我们对这两部分的称呼应该为左侧,右侧,(因为基准值为随机数,不一定为数据的中间值,从而左右两侧的数据量不同)而归并排序中我们将数据进行均分成两部分,我们将这两部分称为左半侧和右半侧,(归并排序中,我们是将数据均分,所有我们可以将之称为左半侧,右半侧)
//归并排序
//将两个分组中的数据归并成一个
//时间复杂度:O(N)
//稳定性:稳定的
void MergeData(int array[], int left, int mid, int right,int temp[])
{
int index1 = left, index2 = mid, index = left;
while (index1 < mid && index2 < right)
{
if (array[index1] <= array[index2])
temp[index++] = array[index1++];
else
temp[index++] = array[index2++];
}
//此时我们不清楚是index1中的数据还是index2中的数据被搬运完
//[left,mid)区间中的数据还没有搬移完
while(index1 < mid)
{
temp[index++] = array[index1++];
}
//[left,right)区间中的数据还没有搬移完
while (index2 < right)
{
temp[index++] = array[index2++];
}
}
void _MergeSort(int array[], int left, int right, int temp[])
{
if (right - left >1)
{
int mid = left + ((right - left) >> 1);
//[left,mid)
_MergeSort(array, left, mid, temp);
//[mid,right)
_MergeSort(array, mid, right, temp);
//将[left,mid)和[mid,right)有序区间进行归并
MergeData(array, left, mid, right, temp);
//归并好之后,有序的数据在temp,将temp中有序的结果拷贝到原数组中
memcpy(array + left, temp + left, (right - left)*sizeof(array[left]));
}
}
//为什么写_MergeSort?
//用户在调用MergeSort时,只需要传递两个参数(类比以上所有方法,都是传递两个参数)
//而_MergeSort需要四个参数
//防止出现问题
//由此得出:有些数据不需要别人给出,我们可以根据已有数据来进行推算得出我们需要的数据时,我们尽量自己推算,防止出现问题
void MergeSort(int array[], int size)
{
int* temp = (int*)malloc(size*sizeof(array[0]));
if (NULL == temp)
return;
_MergeSort(array, 0, size, temp);
free(temp);
}
//快排中时间复杂度会存在最优和最差的情况,归并排序没有最优和最差的场景--》因为:每次划分都是将区间进行均分
//递归函数时间复杂度:递归总的次数*每次递归耗费的时间
//因为归并排序每次都是对区间进行均分,因此数据在分解的时,肯定是平衡二叉树
//高度:log2^N
//时间复杂度:O(Nlog2^N)
//空间复杂度:O(N)
//将递归转换为循环
void MergeSortNor(int array[], int size)
{
int gap = 1;
int* temp = (int*)malloc(size*array[0]);
if (NULL == temp)
return;
while (gap < size)
{
for (int i = 0; i < size;i += 2*gap)
{
int left = i;
int mid = left + gap;
int right = mid + gap;
if (mid > size)
mid = size;
if (right > size)
right = size;
//[left,mid)和[mid,right)每个分组中右gap
MergeData(array, left, mid, right, temp);
}
memcpy(array, temp, size*sizeof(array[0]));
gap *= 2;
}
free(temp);
}
//插入排序,希尔排序,选择排序,堆排序,冒泡排序,快速排序--》内部排序:在排序时需要一次性将所有数据加载到内存中
//归并排序:属于外部排序——》在排序时不需要一次性将所有数据加载到内存中
//统称为比较排序,你只要进行排序,元素与元素之间就需要进行比较
//非比较排序,不需要进行元素之间比较,也可以将数据排列好
//假设:数据密集集中在0~9范围中间
//此时我们创造一段数组,数组的下标就是有序的,而且元素和数组的下标可以对应起来
//设原数组为array,我们创造的数组为Count
//方法:
//1.统计原数组中每个元素出现的次数----Count[array[i]]++
//2.按照统计的次数结果对数据进行回收
//自我理解:
//随便给出一段数子789456123123456789456000
//我们创造个数组(十个单位)
//上面数字中1出现了两次,我们在数组第二个位置写下2,以此类推,我们的到一段数组3222333222
//然后我们在解释我们创造的这个数组即:000112233444555666778899 这个就是我们所求的排列最后顺序
//非比较类排序
//1.计数排序
//场景:数据密集集中在某个范围中
// 计数排序---鸽巢原理
// 时间复杂度:O(N) N: 表示元素的个数
// 空间复杂度:O(M) M:表示数据范围中包含数据的种类的个数
// 稳定性: 稳定
// 场景:数据密集集中在某个范围中
void CountSort(int array[], int size)
{
// 1.找数据范围--->O(N)
int minValue = array[0];
int maxValue = array[0];
for (int i = 1; i < size; ++i)
{
if (array[i] > maxValue)
maxValue = array[i];
if (array[i] < minValue)
minValue = array[i];
}
// 2.计算用来计数的空间个数:maxValue-minValue+1, 申请用来保存计数的空间
int range = maxValue - minValue + 1;
int* temp = (int*)malloc(range*sizeof(int));
memset(temp, 0, sizeof(int)*range);
// 3. 统计区间中每个元素出现的个数--->O(N)
for (int i = 0; i < size; ++i)
temp[array[i] - minValue]++;
// 4. 回收: 按照计数数组下标从小到大进行回收--->O(N)
int index = 0;
for (int i = 0; i < range; ++i)
{
while (temp[i]--)
array[index++] = i + minValue;
}
free(temp);
}
//2.基数排序--桶排序--多关键码排序
//a.高关键码游优先:MSD
//b.低关键码优先:LSD
//多关键码排序
//扑克牌排序:数字+花色信息(♥,♣,♠,♦)
//我们随便给出一些数据,(123 654 789 456 123 789)
//我们根据它的个十百位来进行比较
//个--十--百:低关键码优先LSD
//百--十--个:高关键码优先MSD
//该排序不需要元素之间相互比较--将数据划分为:百十个
//发现:每个位置上的数据都是0~9之间的数字
//因此:给出十个桶,每个桶编号0~9(桶:实际就是一段内存空间)
//LSD:需要用到循环
//for ()//取个十百位置上的数字
//{
//1.获取集合中的每个数据,将该数据按照当前位上的数字放到对应桶中
//例如:个位上的数字是多少,就将数组放到对应的桶中
//2.对每个桶中的数据进行回收
//桶按照桶的编号从小到大回收
//每个桶按照先放入的数据先回收
//}
//基数排序:
//稳定性:稳定的
//时间复杂度:O(MN)--》M:表示最大数据的位数 N:表示数据个数
//空间复杂度:O(N)
//MSD:用不了循环(排列完百位后,排列十位上的数字时,会错乱)
//解决方法:排列完百位后,直接对桶中的数据进行桶内进行是十位上的数据进行桶排序,然后再对十位上的桶进行排序,最后进行整体回收
//
void TestSort()
{
int array[] = { 4, 1, 7, 6, 3, 9, 5, 8, 0, 2 };
PrintArray (array, sizeof(array) / sizeof(array[0]));
//InsertSort(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
/*ShellSort(array, sizeof(array) / sizeof(array[0]));
PrintArray(array, sizeof(array) / sizeof(array[0]));*/
//SelectSort(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//SelectSortOP(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//HeapSort(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//BubbleSort(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//BubbleSortOP(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//QuickSort(array, 0, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//QuickSortNor(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//MergeSort(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
//MergeSortNor(array, sizeof(array) / sizeof(array[0]));
//PrintArray(array, sizeof(array) / sizeof(array[0]));
CountSort(array, sizeof(array) / sizeof(array[0]));
PrintArray(array, sizeof(array) / sizeof(array[0]));
}
//排序的概念:
//排序的稳定性:算法是否稳定,参考:在排序过程中是否隔着区间或者隔着元素来进行交换或者插入
//内部排序和外部排序:目前我们所学的排序中除了归并排序是外部排序,其余都是内部排序
//在排序中需要将所有数据一次性全部加载到内存中,这些都是内部排序,反之则为外部排序
//对与所讲过的排序算法:
//1.原理 2.代码实现 3.时间复杂度 4.空间复杂度 5.稳定性 6.应用场景
//排序的重要性:在正常工作中,我们处理一些实际问题时,排序算法一般不需要我们去写,(对于c语言来说,处理排序算法时,我们需要去写相关代码)其他类型的算法,他们的库函数比较多,我们可以直接应用
//在面试期间,面试官非常喜欢用排序来考察我们的代码功底以及逻辑思维能力----快速排序,堆排序,冒泡排序
stack.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once//防止重复包含
typedef int SDataType;
/*
静态栈
#define MAXSIZE 100
typedef struct Stack
{
SDataType array[MAXSIZE];
int top;//标记栈顶
}stack;
如果在笔试时需要用到栈,自己可以快速定义一个静态栈,不用考虑扩容
*/
typedef struct Stack
{
SDataType* array;
int capacity;//容量
int size;//栈中元素的个数 栈顶
}Stack;
//注意:
//1.栈中没有任意位置的插入和删除操作
//2.栈不需要查找--遍历
//初始化
void StackInit(Stack* ps);
//入栈:尾插
void StackPush(Stack* ps, SDataType data);
//出栈:尾删
void StackPop(Stack* ps);
//获取栈顶的元素
SDataType StackTop(Stack* ps);
//获取栈中有效元素的个数
int StackSize(Stack* ps);
//检测栈是否为空
int StackEmpty(Stack* ps);
//销毁
void StackDestroy(Stack* ps);
stack.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "stack.h"
#include <malloc.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
//初始化
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (SDataType*)malloc(sizeof(SDataType)* 10);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 10;
ps->size = 0;
}
//扩容
void CheckCapacity(Stack* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
//1.开辟新空间
SDataType* temp = (SDataType*)malloc(sizeof(SDataType)*ps->size * 2);
if (temp == NULL)
{
assert(0);
return;
}
//2.拷贝元素
memcpy(temp, ps->array, sizeof(SDataType)*ps->size);
//3.释放就空间
free(ps->array);
//4.使用新空间
ps->array = temp;
ps->capacity *= 2;
}
}
//入栈:尾插
void StackPush(Stack* ps, SDataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size++] = data;
}
//出栈:尾删
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
//获取栈顶的元素
SDataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
//获取栈中有效元素的个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
//检测栈是否为空
int StackEmpty(Stack* ps)
{
assert(ps);
return ps->size == 0;
}
//销毁
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}