文章目录
1,时间复杂度与空间复杂度
2,插入排序
3,希尔排序
4,选择排序
1,单趟排序
2,选择排序PLUS版本
5,冒泡排序
6,快速排序
1,hoare版本
2,挖坑法
前言
本期将学习五种基本的排序方法,学完c语言的基本知识就可以学习这个算法思想,我们会以空间复杂度和时间复杂度来对比这几个算法,我们应该要学会这些算法的思想来帮助我们来破解一些算法的题目得出最优的解法
一,时间复杂度与空间复杂度
时间复杂度的计算(描述算法执行的时间随输入数据的规模增长而增长的变化趋势)通常用大写的字母O进行表示
计算时间复杂度的步骤如下:
1,确定基本操作:
找出算法中最关键的步骤,找到他执行的次数最多的操作(一般为循环和递归的深度)
2,计算基本操作执行的次数:
根据输入的数据的规模N,计算机基本操作执行的次数,这可能涉及到循环和递归中的操作
3,最后用O进行标记,将基本操作的执行的次数用O进行标记,忽略常数项,低阶项,保留最大项
这里我们那插入排序来举一个里子怎么计算时间复杂度即可
以最坏的情况来举例子,也就是完全逆序,循环的外层是需要执行的次数,内存循环执行的是用来移动元素来插入
最坏的打算,完全逆序
内存循环需要执行i次(i是从1~n-1)这个是随着循环次数逐步增加的,所以我们来算内存循环执行的次数就是用等差数列的公式即可,算出的答案为(n-1)n/2,我们忽略常数项和低阶项,所以得出来为n的二次方则空间复杂度则为O(N的二次方)
那为什么这个外出循环不算进去呢?这不也是有次数嘛?其实呀这个为常数时间复杂度就被忽略了,我们来学习常数时间复杂度
O(1)为常数时间复杂度,通常有着几个情况:
1,固定循环的次数,这是固定的次数
2,基本的操作,如赋值运算,四则运算,这也是固定的次数
3,访问数组的元素和列表的元素,通过索引直接访问数组和列表的元素,时间为常数,因为数组和列表元素,在内存是连续储存的,可以通过计算偏移量来访问数组的元素
4,递归调用的栈空间
5,访问哈希表
以最好的情况来举例子,顺序是完全有顺序的
我们可以知道,这个只是遍历一次的,因为后面那个去一个一个去找空根本不用找,直接就是固定在原地的,所以就是n次,所以时间复杂度就是O(N)
一般情况下插入排序不会出现这个最后的情况,哪有全是顺序的呢?所以一般都是O(n的二次方)
空间复杂度
空间复杂度是衡量一个算法在运行过程中占用存储空间大小的度量,它与时间复杂度类似,都是用来描述算法的效率,空间复杂度关注的是算法执行过程中临时占用的存储空间,通常用输入规模的函数来表示
计算空间复杂度的步骤如下:
-
确定基本单位:首先确定算法中占用空间的基本单位,比如变量、数据结构等
-
分析变量:分析算法中的变量,包括局部变量、全局变量等,确定它们的占用空间
-
分析数据结构:分析算法中使用到的数据结构,如数组、链表、树、图等,确定它们的占用空间
-
考虑递归:如果算法是递归的,需要考虑递归调用时的栈空间占用
-
考虑输入输出:有时算法的输入输出也会影响空间复杂度,但通常不考虑输入输出本身占用的空间
-
确定空间复杂度:将以上所有占用的空间加起来,得到算法的空间复杂度
空间复杂度通常用大O表示法来表示,例如O(1)表示常数空间复杂度,O(n)表示线性空间复杂度,O(n^2)表示平方空间复杂度等
二,插入排序
1,步骤(从小到大排序):
1.需要一个有序的数列来进行插入,所以我们默认把第一个元素视为有序的一个数列
2.取下一个元素tem,从已经排好的元素从后面往前排
3.该的元素大于tem的话那么tem就要往后面移动,知道找到比他小的元素
4.找到之后就直接插入到这个小的元素的前面
思想:
插入排序是必须需要一个有序的数列的,然后从无序的数列进行逐个在有序的数列进行插入,所以我们就默认数列里面的第一个元素为有序的数列
我们来举一个例子
这样进行逐个的插入就可以了接下来我们就来用代码实现一下
cpp
#include<stdio.h>
void InsertSort(int *arr,int n) {
for (int i = 0; i < n-1; i++) {
int end = i;
int tem = arr[end + 1];
while (end >= 0) {
if (tem < arr[end]) {
arr[end + 1] = arr[end];
end--;
}
else
break;
}
arr[end + 1] = tem;
}
}
int main() {
int a[6] = { 8,7,5,3,4,1};
int n = sizeof(a)/sizeof(a[0]);
InsertSort(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
我们根据这个代码来看看怎么实现这个插入排序的
对main函数的写法,想必大家都已经熟练掌握了,但是这个插入排序呢?
cpp
int end = i;
int tem = arr[end + 1];
while (end >= 0) {
if (tem < arr[end]) {
arr[end + 1] = arr[end];
end--;
}
else
break;
}
arr[end + 1] = tem;
1,定义:这里定义了一个end,这个是用来整有序序列最后一个值的,然后tem就是在有序数列后面那个待插入的元素
2,while循环:这里是利用end>=0来当条件,当tem小于的话则执行if语句里面的语句,这个意思就是把元素往后面移动,然后进行end--,如果不是的话,则执行else语句,注意这个插入排序是一个有序的数列,所以有一次不符合if语句的条件就可以直接跳出来了,因为后面都不符合,是有序的
3,为什么最后一个要加一个1,因为这个加一个1是在当前这个较小的元素前面进行插入,所以要+1
代码总结
1,我们需要有一个变量保留最后一项这样就方便逐个排序
2,我们用一个tem来表示这个end后面的一个元素,用这个来逐个判断
3,循环我们要知道这个前面插入的是一个有序的序列,所以我们只要if语句那个不成立,后面也就是一样的,直接跳出循环来进行赋值
4,外层循环记得要n-1
时间复杂度:
最坏的情况就是O(N的2次方),此时待排序的是逆序,或者接近逆序
最好的情况就是O(N的1次方),此时待排序的是顺序,或者接近逆序
三,希尔排序
步骤:
这里比较抽象,我们结合图来讲,如果在网上看动图,如果没有看懂也没关系,我们来仔细分析一下
这里是希尔排序的详细解释,我们可以理解一下,这里的gep是任意取的,但是我们用一个除法,进行有规律的解更好,也很具有可读性,编写代码的时候也是十分有逻辑的,下面我们用代码实现一下这个希尔排序
cpp
#include<stdio.h>
void ShellSort(int A[],int n) {
int gep, j, i;
int num=0;
for (gep = n / 2; gep <= 1; gep = gep / 2) {
for (i = gep; gep <= n; gep++){
if (A[i] > A[i - gep]) {
num = A[i];
}
for (j = i - gep; num < A[j] && j>0; j = j - gep) {
A[j + gep] = A[j];
A[j] = num;
}
}
}
}
int main() {
int a[6] = { 8,7,5,3,4,1};
int n = sizeof(a)/sizeof(a[0]);
ShellSort(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
这里又是一个也有很多的难点,我们逐步来分析
第一个循环是每一个gep的取值都要详细的说明,所以为什么要有规律的取gep就是因为这样的话你的代码具有很强的可读性
第二个循环是针对每一组的数据的调序的
第三个循环是利用一个插入排序来求解的,num<A[ j ]这个玩意就是如果有这个的话就是一直我那个后面进行移动元素,然后把num放到那个元素里面去
代码总结
我们子啊写这个代码的时候,我们要融入插入排序的思想,进行排序,然后有一个循环控制gep,一个循环控制每一组,一个循环控制排序
时间复杂度平均:O(N^1.3)
空间复杂度:O(1)
四,选择排序
1,单趟选择排序法
步骤:
先取一个元素,然后在其他元素里面去寻找找到最值,放到取到的元素里面
思想:
先取a[ 0 ],然后在a[ 1 ]~a[ n ]去寻找到的最值(这里看用户的需求,是按照从小到大还是从大到小,如果是从小到大就是寻找到最小值,然后把最小值放到a[ 0 ]处)
再取a[ 1 ],然后在a[ 2 ]~a[ n ]取寻找最值,然后把最值放入到a[ 1 ],然后后面就是重复上面的动作了
这个图就是十分简单了,所以这里只用理解这个思想和步骤即可
我们再来看一下代码该怎么写,用这个思想
这里有两种代码,一个是我写的,一个是网上的,我们来看看这两个代码有什么不同之处
我写的代码
cpp
#include<stdio.h>
void SelectSort(int A[],int n) {
int k=0,i=0;
for(k=0;k<=n-1;k++) {
int idenx = k;
for (i = k + 1; i <= n-1; i++) {
if (A[i] < A[idenx]) {
idenx = i;
}
}
int temp = A[k];
A[k] = A[idenx];
A[idenx] = temp;
}
}
int main() {
int a[6] = { 5,2,3,7,6,4};
int n = sizeof(a)/sizeof(a[0]);
SelectSort(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
这个代码我就把这个思想转换到代码里面来实现了,我这里使用下角标来寻址,首先用一个外循环来定位每一个就是a【0】,a【1】等等,然后用一个内循环来寻找最小值的下角标,然后进行换值操作
这个是网上的代码
cpp
#include<stdio.h>
#include<utility>
using namespace std;
void SelectSort(int A[],int n) {
for (int i = 0; i < n; i++) {
int start = i;
int min = i;
while (start < n) {
if (A[min] > A[start]) {
min = start;
}
start++;
}
swap(A[i], A[min]);
}
}
int main() {
int a[6] = { 5,2,3,7,6,4};
int n = sizeof(a)/sizeof(a[0]);
SelectSort(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
这个是利用c++这个的函数swap,这个是用来交换这个值的,我们先用start和min来存储这个下角标,然后用while循环来寻找这个i值存储到min,最后用swap来交换这个值
我们来对代码进行一下总结,我们要用一个数组遍历这个次数,就是有n个元素,要遍历n-1次,然后再用另外一个循环来寻找这个最值的下角标,最后进行交换即可
时间复杂度:最坏情况:O(N^2)
最好情况:O(N)
空间复杂度:O(1)
2,选择排序PLUS版本
我们在序列中依次找到最大值和最小值,然后放入头和尾
cpp
#include<stdio.h>
#include<utility>
using namespace std;
void SelectSortPLUS(int A[],int n) {
int begin = 0; int end = n - 1;
while (end > begin) {
int Maxi = begin;
int Endi = begin;
for (int i = begin + 1; i <= end; i++) {
if (A[i] > A[Maxi]) {
Maxi = i;
}
if (A[i] < A[Endi]) {
Endi = i;
}
}
swap(A[begin], A[Endi]);
if (begin == Maxi) {
Maxi = Endi;
}
swap(A[end], A[Maxi]);
++begin;
--end;
}
}
int main() {
int a[6] = { 5,2,3,7,6,4};
int n = sizeof(a)/sizeof(a[0]);
SelectSortPLUS(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
代码总结:
我们这里定义一个 begin end来存放这个开头和结尾,定义一个Maxi Endi来存放这个最大值和最小值,然后要考虑一个情况,当我们把这个最小值和数组的0下角标互换之后,这个最大值如果在0那个位置,就要把Maxi的位置改为Endi,然后再进行互换
五,冒泡排序
这个冒泡排序非常的简单,这个就是把值逐渐冒泡冒上来
由于过于简单所以直接上代码看
cpp
#include<stdio.h>
#include<utility>
using namespace std;
void SortPLUS(int A[],int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (A[j] > A[j + 1]) {
int temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
}
}
}
}
int main() {
int a[6] = { 5,2,3,7,6,4};
int n = sizeof(a)/sizeof(a[0]);
SortPLUS(a, n);
for (int i = 0; i < 6; i++) {
printf("%d ", a[i]);
}
}
这里的冒泡排序这里的j<n-1-i,因为这里是因为没冒泡一次就是固定了一个数据,所以每次就是减少一个数据,然后进行互换值,注意这个if语句,这个if语句,里面是如果一直大于的话就一直把这个值冒泡上去,如果不是则为该j的值来进行冒泡,把最大的冒泡上去
六,快速排序
1,hoare版本
步骤
这个是选出一个key的值(一般选最左边和最右边,也可以任意取,但是最左边和最右边更加方便)
然后定义一个begin和end,begin从左往右走,end从右往左走
然后让begin找到比key大的数字,让end找到比key小的数字进行互换,注意选取最左边,end先走,选取最右边最右边先走
我们以一个例子来分析一下这个hoare版本
这个是分析序列的方法,利用快速排序的方法,来解决这个排序的问题 ,这里就是如果取走最右边的值作为key,那么end先动,end那边都是寻找相比于key要小的数字,因为我们要有序嘛,当找到之后,begin再动,寻找到相比于key大的值,然后于end进行交换,然后呢就是在让end再动重复上述过程,知道begin于end相撞之后,就直接把相撞位置的值与key互换,然后在相撞的位置提出来,变成两个子序列,然后再对两个子序列进行重复的操作
接下来我们看代码来理解
cpp
//快速排序 hoare版本(左右指针法)
void QuickSort(int* arr, int begin, int end)
{
//只有一个数或区间不存在
if (begin >= end)
return;
int left = begin;
int right = end;
//选左边为key
int keyi = begin;
while (begin < end)
{
//右边选小 等号防止和key值相等 防止顺序begin和end越界
while (arr[end] >= arr[keyi] && begin < end)
{
--end;
}
//左边选大
while (arr[begin] <= arr[keyi] && begin < end)
{
++begin;
}
//小的换到右边,大的换到左边
swap(&arr[begin], &arr[end]);
}
swap(&arr[keyi], &arr[end]);
keyi = end;
//[left,keyi-1]keyi[keyi+1,right]
QuickSort(arr, left, keyi - 1);
QuickSort(arr,keyi + 1,right);
}
这里用到了递归,我们来仔细分析一下
这个就是递归的基准情形,在机械工业革命出版的数据结构预算法中,这个是一定要有的,要不然将陷入无线的递归,这个基准情形就是我们在平时那种的写法,让他不递归(这里是结束的条件当只有一个数字或者没有区间则直接跳出)
这个是我们根据上面那个思想来设置的变量,定义一个keyi,left,right,这里的keyi是选择左边的,所以我们的移动方式是要从右边开始,这里的left是保存值,然后让begin和end移动
这个代码我们看到end--,说明一开始的循环是先找到右边的最想只,然后里面的条件就是end>key对应的数组的值,然后下面也一样,当两个都通过循环找到了,我们就可以用这个两个下架表通过swap函数来进行交换了
这个代码就是把相撞的那个值与key进行互换,但是这个key为什么要等于end呢,就是我们我们在递归的时候就更加具有可读性,而且也把我们的思想体现出来,因为我们是根据key来分左右子序列的,我们来看,一开始的把left存进去,key-i存进去,这个就是一个左边一个右边,那么下面那个也是一样的,这个就可以对子序列进行操作了
代码总结:这个就是我们先要存储这个left和right这样递归才可以找到对应的值,然后利用begin和end来进行移动找值,需要注意的就是begin先动还是left先动然后再进行互换值,直到相撞之后与key互换值,然后注意递归的写法即可
挖坑法就是多练一个变量而已,就是把a[ 0 ]空出来了用来存放值,然后不断的进行互换
cpp
//快速排序法 挖坑法
void QuickSort1(int* arr, int begin, int end)
{
if (begin >= end)
return;
int left = begin,right = end;
int key = arr[begin];
while (begin < end)
{
//找小
while (arr[end] >= key && begin < end)
{
--end;
}
//小的放到左边的坑里
arr[begin] = arr[end];
//找大
while (arr[begin] <= key && begin < end)
{
++begin;
}
//大的放到右边的坑里
arr[end] = arr[begin];
}
arr[begin] = key;
int keyi = begin;
//[left,keyi-1]keyi[keyi+1,right]
QuickSort1(arr, left, keyi - 1);
QuickSort1(arr, keyi + 1, right);
}
我们也来分析一下吧
这个就是基准情形和一些变量的定义,这个key存储了arr[ begin ],那么arr[ begin ]那么这个就是一个坑位了
这里无非就是,先end起手,因为从左边开始的,找到之后把end放入坑里面(a[ 0 ]),放入后后面再到begin找到,找到之后就是把begin放到end里面去,这个时候begin开始空了然后再去end里面找较小的值,找到放入到begin中,然后begin开始找,找到之后放入end里面,这个过程始终是有一个坑的,然后就是相撞之后,把key填入到那个坑即可
总结
我们学习了各种各样的算法来解决问题,我们要熟练掌握每个方法
1,插入排序就是要的一个end来逐步的进行元素的下角标的减少,然后根据end+1来进行插入,这里需要注意的是前面为有序的数列
2,希尔排序就是要gep,外围的循环就是要用来gep的计算,内部的循环就是第一个是要用来分组的用gep,然后最后一个循环就是一个插入排序,这里的for循环写入一个a[j]>num,这个是由于前面为有序的序列,然后里面就是就是要不断的推进与赋值
3,选择排序就是要有一个起始的下角标,然后用循环找到最值的下角标,然后进行互换即可,PLUS版本就是多了一个if和一个判断如果最值在最开始的地方的话就要取走end的值
4,冒泡排序就是要逐步冒泡上去注意第二个循环要-i,把固定的元素给减掉,if语句的互换是可以判断最值的,因为在排序的过程中,我们如果符合if语句就一定暂时是最值,不成立,下一次就是另外一个了
5,快速排序就是要把那个key学会找和begin和end是怎么移动的,所以我们要好好理解这个begin和end是怎么进行移动的才可以正确理解这个