1、插入排序
插入排序首先从第二个元素开始标记key 标记key的前一个元素为end ,key从第二个元素开始,那么end即为第一个元素,两者进行比较,如果我的key比end位置的元素小,那么就将end位置的元素往后搬移即放入end+1位置也就是1的位置,然后对end--;如果end来到了-1的位置那不在进行比较,将我的key插入到end+1的位置即可,也就是当前的0号下标。
我们的循环条件也就是上面框中的内容 我的end要>=0 并且key小于end位置的元素,就把end位置的元素往后搬移。key 走到下一个位置,end也继续指向key之前的位置以此类推。
代码:
// 时间复杂度:O(N^2)
// 空间复杂度:O(1)
// 稳定性:稳定
// 应用场景:适合数据接近有序 或者 数据量少
void InsertSort(int array[], int size)
{
// 从第二个元素开始
for (int i = 1; i < size; ++i)
{
// 初始化key元素与end元素一前一后
int key = array[i];
int end = i - 1;
// 当end不为负数 并且后面的key比前面的end元素小
// 将前面end位置的元素移动到end的后一个位置
// end-- 向前移动
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
// 此时end为-1 或者 key的元素大于end的位置元素
// 直接将key放入 end+1 位置即可
array[end + 1] = key;
}
printf("插入排序:");
Print(array, size);
}
2、希尔排序
希尔排序为插入排序的一种优化,引入了一个值gap,书籍中查到gap一般取size元素除以3+1;
// 时间复杂度:O(N^1.25)~O(1.6N^1.25)
// 空间复杂度:O(1)
// 稳定:不稳定
// 应用场景:数据杂乱或者数据量较大时
void ShellSort(int array[], int size)
{
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = gap; i < size; ++i) // 依次获取到array数组中的每个元素进行插入
{
// 单个元素的插入过程
int key = array[i];
int end = i - gap;
// 找待插入元素在array中的位置
while (end >= 0 && key < array[end])
{
// 往后搬移到当前分组的下一个位置
array[end + gap] = array[end];
end -= gap; // 获取当前分组的前一个位置的元素
}
// 插入key
array[end + gap] = key;
}
//gap -= 1;
}
printf("希尔排序:");
Print(array, size);
}
3、选择排序
选择排序就是每次遍历一遍找到最大的元素,循环结束之后把这个标记的masPos位置的元素与序列末尾元素进行一次交换,然后再从剩余的元素中再次寻找最大的元素,放到序列的末尾,以此类推。
原地操作几乎是选择排序的唯一优点,当空间复杂度要求较高时,可以考虑选择排序。其次就是在检查给定的列表是否已经排序时可以适用该算法。
// 时间复杂度: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)
{ // 逐个与后方元素进行比较
// 如果后面j位置元素的值大于maxPos位置的元素
if (array[j] > array[maxPos])
{ // 将maxPos位置重新更改
maxPos = j;
}
}
if (maxPos != size - i - 1)// 如果maxPos标记位置不是最右侧
{ // 利用swap函数进行交换
Swap(&array[maxPos], &array[size - i - 1]);
}
}
}
4、选择排序优化
选择排序的优化 选择排序一次只能找到一个最值元素,优化的想法是对序列进行遍历,同时找到并且标记最大元素与最小元素的位置。
// O(N^2)
// 选择的趟数减少了一半,但是元素的比较次数实际并没有减少
// 时间复杂度:O(N^2)
// 空间复杂度:O(1)
// 稳定性:不稳定
void selectOP(int array[], int size)
{
// 首尾位置的定义
int begin = 0;
int end = size - 1;
// begin < end 即就可以进入循环
while (begin < end)
{
// 用minPos标记最小位置
// 用maxPos标记最大位置
int minPos = begin;
int maxPos = begin;
// 利用index 来进行遍历
int index = begin + 1;
// 当index遍历的位置没有超过end时,进入循环
while (index <= end)
{
// 如果index位置的元素小于min位置的元素 min标记进行更新
if (array[index] < array[minPos])
minPos = index;
// 如果index位置的元素小于min位置的元素 min标记进行更新
if (array[index] > array[maxPos])
maxPos = index;
++index;
}
// 将max标记的元素放入end位置
if (maxPos != end)
Swap(&array[maxPos], &array[end]);
// 如果最小元素如果恰巧在end位置,上面的交换结束之后
// 最小的元素的位置就发生了改变,此时必须要及时更新minPos
if (minPos == end)
minPos = maxPos;
// 将min标记的元素放入begin位置
if (minPos != begin)
Swap(&array[minPos], &array[begin]);
// 两边的标记位置变动
begin++;
end--;
}
printf("选择排序优化:");
Print(array, size);
}
5、冒泡排序
直接附上代码就ok
每一趟将最大的元素放到末尾
void bubblesort(int array[], int size)
{
for (int i = 0; i < size - 1; ++i)
{
for (int j = 0; j < size - i - 1; ++j)
{
if (array[j] > array[j + 1])
{
Swap(&array[j], &array[j + 1]);
}
}
}
printf("冒泡排序:");
Print(array, size);
}
6、堆排序
// 首先呢 我们需要书写堆排序的核心函数 向下调整函数
// 时间复杂度:O(NlogN)
// 空间复杂度:O(1)
// 稳定性:不稳定
// 应用场景:部分排序 或者 和其他排序共同使用
这里第一个注意点是循环进入判断child标记左右孩子时,如果右孩子存在则需要判断,如果右孩子不存在就不需要判断child位置
第二个注意点就是 应该没有了 就简单说一下原理吧,不然这一点显得很呆,首先child标记参数parent*2+1;这也是一个特性,如果child小于结点个数则进入循环,判断是否需要将child结点进行变动,因为child是要指向俩个孩子中更大的小的那一个,我们才能在左右孩子还有parent中找到最小的那个结点将他放入parent的位置,当找到了这个孩子child时,我们对parent与该孩子进行判断如果parent结点的值大于我们child结点的值我们就进行这俩个结点值得交换,然后将paren更新为child ,child更新为下一个孩子结点,然后再继续向下调整,如果满足我的parent值小于child那我们的向下调整已经完成,return返回即可。
void AdjustDown(int array[], int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && array[child] > array[child + 1])
{
child = child + 1;
}
if (array[parent] > array[child])
{
Swap(&array[parent], &array[child]);
parent = child;
child = 2 * child + 1;
}
else
return;
}
}
再来看我们的堆排序,知道了向下调整,那么堆排序中就是对向下调整的不断调用。
建堆之后,将堆顶元素与末尾元素进行交换,然后将新的堆顶元素进行向下调整即可。
void Heapsort(int array[], int size)
{
// 堆排序
// 首先进行建堆 降序建小队 升序建大堆
for (int root = (size - 2) / 2; root >= 0; --root)
{
AdjustDown(array, size, root);
}
int end = size - 1;
while (end)
{
// 利用堆删除的思想
// 将堆尾元素与堆顶元素进行交换
Swap(&array[end], &array[0]);
// 再将堆顶进行向下调整即可 如此循环将每个元素进行排序
AdjustDown(array, end, 0);
end--;
}
Print(array, size);
}
7、快速排序
快排的一个重要思想就是利用递归来进行实现
首先我们需要利用区间划分,这时候我们就需要一个划分区间的函数
其次将左半部分区间进行快排,再将右半部分进行快排即可。
void qsort(int array[], int left, int right)
{
if (right - left <= 1)
return;
// 利用partion函数找到数组的中间结点
// 利用基准值分为左右俩部分 并返回基准值的下标
int div = partion3(array, left, right);
// 递归排基准值div的左侧
qsort(array, left, div);
// 递归排基准值div的右侧
qsort(array, div + 1, right);
}
那么基准值如何找呢?
// 划分方式一:hoare
// 时间复杂度:O(N)
思想:首先标记最右侧元素为key,进入循环 如果begin位置元素小于key则begin++,当我们的begin循环停止的时候要么begin和end重合了,要么就是我的begin位置的元素他大于key
相同的end位置的循环停止了说明end位置与begin重合了,要么就是我的end指向了小于key的元素,俩个循环都停下来了 如果不是重合了那么就将我的begin与end进行交换。
当一次次的循环交换结束后我的begin最终与end重合了 那begin与end指向了左右两半区间,即左区间为比key小的元素,而右区间为比key大的元素。不要忘记将重合之后如果不是在end位置重合的我们就将begin与right-1位置元素进行交换,即这个right-1标记了我们需要的基准值,最终返回begin即可。
int partion1(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int key = array[end];
while (begin < end)
{
// 进入循环 找到begin位置元素大于key的位置
while (begin < end && array[begin] <= key)
begin++;
// 循环找到比end位置元素小于key的位置
while (begin < end && array[end] >= key)
end--;
// 如果曼如begin < end 基础条件
// 进行俩个值得交换
if (begin < end)
Swap(&array[begin], &array[end]);
}
// 当begin与end位置重合的时候进行 将基准值的传递
// 使得基准值的左侧均小于基准值 使得基准值的右侧均大于基准值
Swap(&array[begin], &array[right - 1]);
return begin;
}
This is 法一,Let's play 法二:挖坑法
当我们把end位置元素4赋给key时这时候我们就多出了一个坑位!
begin从左遍历遇到大于key的元素位置也就是 9 位置处 我们将9 填入刚才的坑位中,这时9位置元素多了一个坑位!
然后end又从右侧开始遍历直到遇见小于key的元素位置,到了3处停了下来,把3插入前面的9的那个坑位中,我们的3这里又多出来一个坑位!
再从begin开始找大于key的元素,到了5 这里停下来,把5插入后面3的坑位,5这里又多出来一个坑位!
end继续遍历此时end与begin相遇了,我们把key的值放入这个重合的坑位中。
返回begin位置 即将序列分为左右俩个区间,左区间的元素都小于基准值,右半区的元素都大于基准值。
int partion2(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int key = array[end];
while (left < end)
{
// 挖坑法:从left位置元素往后遍历 若是begin位置的元素大于key
// 就将此时begin位置的元素填入end位置 end指针--向前移动
while (left < end && array[begin] <= key)
begin++;
if (left < end)
array[end--] = array[begin];
// 完成后进入这次循环 左边的begin位置有多出来这么一个坑位
// 当end位置遍历至比key的元素小的位置时 将end位置元素填入之前的begin位置 begin 后移
while (left < end && array[end] >= key)
end--;
if (left < end)
array[begin++] = array[end];
}
// 最后循环结束 说明begin和end到达同一位置 将key元素给给beigin
array[begin] = key;
return begin;
}
法三:左右指针法
key依然标记最右侧元素,cur标记第一个元素,prev标记第一个元素的前一个
当cur没有遍历到最后一个元素时进入循环:
这里有个短路 如果&&前面的成立 则执行后面的语句并且无论后面语句成立或者不成立我的prev都会往后走一步。
所以第一次: cur指向9, prev指向1
第二步:cur的值大于key prev不进行移动 cur向后移动
第三次:cur指向2 2 < key,prev向后移动到9的位置,并且进行交换后 cur移动到5.
第四、五次: 5>key 7>key,,cur向后移动俩次
第六次:cur指向3 prev向后移动,交换prev与cur 并将cur向后移动
第七次:cur遍历完成循环退出 cur指向了 4 后面的不存在的位置。
最后判断如果prev的下一个元素不是最后一个元素,那么就将基准值4与prev的下一个元素进行交换即可。
返回基准值位置prev;
int partion3(int array[], int left, int right)
{
int cur = left;
int prev = left - 1;
int keyindex = GetMiddle(array, left, right);
Swap(&array[keyindex], &array[right - 1]);
int key = array[right - 1];
while (cur < right)
{
// cur 之前的元素都小于key cur之后的元素都大于key
// cur往后移动 遇到一个比key小的值就利用prev将cur位置的元素交换到前面去
// 并且有pre和 cur均往后移动
if (array[cur] < key && ++prev != cur)
Swap(&array[cur], &array[prev]);
++cur;
}
// 如果prev的位置不在最后一个元素 就将
if (++prev != right)
Swap(&array[prev], &array[right - 1]);
return prev;
}
我们每次都用到了最后一个元素作为我们的key,如果这个key为较偏中间的那个元素那是比较好的,可是如果出现了最大或者最小元素出现在了最右侧,那么排序又不容易了,所以我们对key取值在进行一次优化:
GetMiddle:找到left right-1 和最终间mid元素,他们三个进行比较,返回三个里面最小的那个元素。三数取中法优化快排: 主要是降低拿到极值的概率。
int GetMiddle(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 mid;
else if (array[mid] > array[right - 1])
return right - 1;
else
return mid;
}
else // right-1 mid left
{
if (array[mid] < array[right - 1])
return right - 1;
else if (array[mid] > array[left])
return left;
else
return mid;
}
}
8、快排的非递归使用
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 = Partion(array, left, right);
// 基准值的左侧[left, div)
// 基准值的右侧[div+1,right)
// 将基准值的右侧压栈
// 先压右侧然后再压区间的左侧
StackPush(&s, right);
StackPush(&s, div + 1);
// 将基准值的左侧压栈
// 先压右侧然后再压区间的左侧
StackPush(&s, div);
StackPush(&s, left);
}
StackDestroy(&s);
}
9、归并排序
malloc开辟一个数组空间
我的所有分划出来的区间进行排序后都会放入这个数组中。
void MergeSort(int* array, int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (NULL == temp)
{
assert(0);
printf("fail\n");
}
_MergeSort(array, 0, n, temp);
}
主函数:
void _MergeSort(int array[], int left, int right, int *temp)
{
// 如果区间元素个数小于一个即不进行排序
if (right - left <= 1)
return;
// 将数组分为左右俩个区间
int mid = left + ((right - left) >> 1);
// 递归遍历左右区间
_MergeSort(array, left, mid, temp);
_MergeSort(array, mid, right, temp);
// 将左右区间元素进行归并
MergeData(array, left, mid, right, temp);
// 将辅助空间中的元素拷贝到原空间
memcpy(array + left, temp + left, (right - left) * sizeof(int));
}
合并排序区间:用到的就是有序数组合并的知识
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;
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
temp[left++] = array[begin1++];
else
temp[left++] = array[begin2++];
}
while(begin1<end1)
temp[left++] = array[begin1++];
while(begin2<end2)
temp[left++] = array[begin2++];
}
我们开辟的数组空间每次递归的时候用来保存我们的元素,将序列用mid来进行二等分,然后再递归将左半部分进行二等分直到递归到只有一个元素的时候我们在递归他的右区间,右区间只有一个元素时我们在返回,不断的递归……递归结束后将俩个数组进行有序合并,合并到temp之后将将元素memcpy到array数组中。
注意在每次合并返回的时候我们用到的都是同一个temp,只是放入temp的位置不同,将来自同一个数组array中的不同区间的元素进行合并放入同一个数组temp中
10、递归排序的非递归实现
利用循环来进行实现
// 归并排序非递归实现
void MergeSortNonR(int* array, int n)
{
int* temp = (int*)malloc(sizeof(int) * n);
if (NULL == temp)
{
assert(0);
printf("fail\n");
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int left = i;
int mid = left + gap;
// 注意判断mid和right的位置是否存在越界
if (mid > n)
mid = n;
int right = mid + gap;
if (right > n)
right = n;
MergeData(array, left, mid, right, temp);
}
memcpy(array, temp, sizeof(int) * n);
gap <<= 1;
}
free(temp);
}
11、计数排序
// 计数排序
// 时间复杂度:O(N) N表示元素的个数
// 空间复杂度: O(M) M就是区间中元素的个数
// 稳定性:稳定的
// 应用场景:数据密集集中在某个范围内
void CountSort(int array[], int size)
{
// 1. 假设没有告诉区间中数据的范围,如果告诉了第一步就不需要
// 统计数据的范围
// 比如:数据密集集中在某个范围内---此时就需要统计范围
// 数据秘密集中在90~99之间,就不需要统计范围
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);