【初阶数据结构与算法】第十篇——八大排序算法(头脑风暴逻辑分析+动图详解一看就会+代码分析信手捏来)

🏆个人主页:企鹅不叫的博客

​ 🌈专栏

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【初阶数据结构与算法】第一篇:算法中的时间复杂度和空间复杂度

【初阶数据结构与算法】第二篇:顺序表

【初阶数据结构与算法】第三篇:单链表

【初阶数据结构与算法】第四篇:链表面试题详解

【初阶数据结构与算法】第五篇:双链表

【初阶数据结构与算法】第六篇:栈和队列(各个功能实现+练习题包含多种方法)

【初阶数据结构与算法】第七篇:二叉树和堆的基本概念+以及堆的实现

【初阶数据结构与算法】第八篇——二叉树的顺序结构的应用(堆排序+TOPK问题)

【初阶数据结构与算法】第九篇——二叉树(链式结构实现+四种遍历方式+基本操作实现+基本练习详解)


文章目录


前言


🌏一、排序介绍

🍯1.排序概念

⭐️ 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
⭐️ 排序的稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

⭐️内部排序 :数据元素全部放在内存中的排序。
⭐️外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

🍯2.排序分类

🌏二.插入排序

🍯1.直接插入排序

🍍基本思想

⭐️现在有一个有序的区间,我们插入一个数据,保持它依旧有序

⭐️一般地,我们把第一个看作是有序的,所以我们可以从第二个数开始往前插入,使得前两个数是有序的,然后将第三个数插入直到最后一个数插入。

🍍实现过程

⭐️单趟排序:首先选中end+1下标位置的数据存放到tmp中之后依次从end位置开始向前比较,直到end小于0,每个数进行比较,最后将tmp插入即可

⭐️整合: 总共有n个数,所以需要排序n-1
在最后一趟开始前,所有元素不一定在最终位置上,例如
给出数列45 80 48 40 22 78
第一趟结果45 80 48 40 22 78(排第一个元素)
第二趟结果45 80 48 40 22 78(排第一第二个元素)
第三趟结果45 48 80 40 22 78(排第一第二个元素第三个元素)

void InsertSort(int* a, int n) {
    //有n个数据只用n-1趟排序
    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 + 1] = a[end];
                end--;
            }
            else {
                //当插入的数据是最小值的话,end移动到0位置处理完后,所有数据都往后移完了
                //那下一次end为-1时,结束循环没有成功插入数据
                //a[end + 1] = tmp;
                break;
            }
            a[end + 1] = tmp;
        }
    }
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:
O ( N 2 ) O(N^2) ON2
第一趟end最多往前移动1次,第二趟是2次……第n-1趟是n-1次,所以总次数是1+2+3+……+n-1=n*(n-1)/2,所以说时间复杂度是O(N2)

最好情况:O(N)顺序

最坏的情况:O(N2)逆序

⭐️空间复杂度:
O ( 1 ) O(1) O(1)
没有开辟额外空间

⭐️稳定性:

直接插入排序在遇到相同的数时,可以不移动,就可以保持稳定性了,所以说这个排序是稳定的

🍯2.希尔排序

🍍基本思想

⭐️希尔排序是建立在直接插入排序之上的一种排序,希尔排序的思想上是把较大的数尽快的移动到后面,把较小的数尽快的移动到前面。所以先预排序使数据接近有序,然后再直接插入排序。

⭐️先选定一个整数,把待排序数列中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。(直接插入排序的步长为1),这里的步长不为1,而是大于1,我们把步长这个量称为gap,当gap>1时,都是在进行预排序,当gap==1时,进行的是直接插入排序。

⭐️如果gap越小,说明,数组越接近有序,如果gap越大,大的数据可以更快的到后面,小的数据可以更快的到前面,然是整个数组越不接近有序

希尔排序动图

🍍实现过程

⭐️单趟排序,和直接插入差不多,原来是gap == 1,现在是gap了。

//单组
int end = 0;
int tmp = a[end + gap];
while (end >= 0)
{
	if (a[end] > tmp)
	{
		a[end + gap] = a[end];
		end -= gap;
	}
	else
	{
		break;
	}
}
a[end + gap] = tmp;

⭐️首先对于每一组进行排序,之后再进行下一组排序

// gap组
for (int j = 0; j < gap; j++)
{
	int i = 0;
	for (i = 0; i < n-gap; i+=gap)
	{
		int end = i;
		int tmp = a[end + gap];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + gap] = a[end];
				end -= gap;
			}
			else
			{
				break;
			}
		}
		a[end + gap] = tmp;
	}
}

⭐️所有数据一起排序,不再是一组一组数据排序,少嵌套一层循环

// 一起预排序
int i = 0;
//最后一个数据下标是n-1,那么我们只要到n-1-gap下标循环后就截止
for (i = 0; i < n - gap; 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=1时,直接插入排序,当gap > 1时,预排序,并且gap越大,预排序越快,排序结果越不接近有序,gap越小,排序越慢,预排序后越接近有序

⭐️对于控制gap,我们可以让最初的gap控制为n,最后一次gap控制为1就可以了,我们可以gap /= 2(最后一个是偶数,也是1),也可以
g a p = g a p / 3 + 1 gap = gap / 3+1 gap=gap/3+1
加1是为了,防止gap为0

void ShellSort(int* a, int n) {
    int gap = n;
    //注意gap>1,可以保证最后一次是2
    while(gap > 1){
        gap = gap / 3 + 1;
        //最后一个数据下标是n-1,那么我们只要到n-1-gap下标循环后就截止
        for (int i = 0; i < n - gap; ++i) {
            int end = i;
            int tmp = a[end + gap];
            while (end >= 0) {
                if (a[end] > tmp) {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:
O ( N 1.3 ) O(N^{1.3}) O(N1.3)

当gap很大的时候几乎都跳到后面去了,差不多时O(N),很小差不多也是O(N)

平均下来是O(N1.3)

⭐️空间复杂度:
O ( 1 ) O(1) O(1)
⭐️稳定性分析:

相同的数,可能被分到不同的gap当中,不稳定

🌏三.选择排序

🍯1.直接选择排序

🍍基本思想

⭐️ 每次从数组中选择最大的一个数和最小的一个数,把他们放到开头和结尾,然后再取次大的一个数和次小的一个数,放到开头第二或者结尾第二,依次这样进行,直到只剩下一个元素或者没有。

🍍实现过程

⭐️首先选出第一个元素left和最后一个元素right,同时从两头二分数组,创建当前最小下标和最大下标,选出最小下标mini和最大下标maxi,然后交换left下标的值和mini下标的值,再交换right下标的值和maxi下标的值的时候,要判断left下标是否和maxi下标相等,防止接下来的交换将maxi掉包
和直接插入不同的是,每趟排列会选出最小的放前面,第一趟选出最小的放最前面,第二趟选出次小的放第二个位置

void SelectSort(int* a, int n){
    int left = 0, right = n - 1;
    while (left < right) {
        //初始化记录最大下标和最小下标
        int mini = left, maxi = left;
        //i从left+1开始,是因为mini从left开始了,然后两边都是闭区间
        for (int i = left + 1; i <= right; ++i) {
            //如果i下标对应的数比mini下标对应数大,则交换
            if (a[mini] > a[i]) {
                mini = i;
            }
            如果i下标对应的数比maxi下标对应数小,则交换
            if (a[maxi] < a[i]) {
                maxi = i;
            }
        }
        Swap(&a[left], &a[mini]);
        //left和maxi重叠,说明maxi被换到mini原来的位置上去了,修正一下maxi即可
        //防止left和maxi相等时,mini与left交换会导致maxi的位置发生变化
        if (left == maxi) {
            maxi = mini;
        }
        Swap(&a[right], &a[maxi]);
        right--;
        left++;
    }
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:

O(N^2)

第一趟遍历n-1个数,选出两个数,第二趟遍历n-3个数,选出两个数……最后一次遍历1个数(n为偶数)或2个数(n为奇数),所以总次数是n-1+n-3+……+2,所以说时间复杂度是O(n^2)

⭐️空间复杂度:

O(1)

⭐️稳定性分析:

下面就是红色的5和黑色的5相对顺序变了不稳定

🍯2.堆排序(详细介绍点这里

🍍基本思想

⭐️首先建立堆(升序建大堆,降序建小堆),然后调整数据

🍍实现过程

void AdjustDown(int* a, size_t size, size_t root)
{
    size_t parent = root;
    size_t child = parent * 2 + 1;
    while (child < size)
    {
        // 1、选出左右孩子中小的那个,注意,child+1 < size要写在前面,防止越界
        if (child + 1 < size && a[child + 1] > a[child])
        {
            ++child;
        }
        // 2、如果孩子小于父亲,则交换,并继续往下调整
        if (a[child] > a[parent])
        {
            Swap(&a[child], &a[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

void HeapSort(int* a, int n)
{
    // 向下调整--建堆 O(N)
    for (int i = (n - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(a, n, i);
    }
    for(int i = n-1; i > 0; --i){
        swap(a[0], a[i]);
        AdjustDown(a, i, 0);
    }
    //size_t end = n - 1;
    //while (end > 0)
    //{
    //    Swap(&a[0], &a[end]);
    //    AdjustDown(a, end, 0);
    //    --end;
    //}
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:
O ( N l o g N ) O(NlogN) O(NlogN)
⭐️空间复杂度:
O ( 1 ) O(1) O(1)
⭐️稳定性分析:

升序大堆,两个数都是8,之后最上面8插入到最后一个顺序就颠倒了不稳定

image-20220426170212908

🌏四.交换排序

🍯1.冒泡排序

🍍基本思想

⭐️以升序为例,每一趟的冒泡排序都是把一个最大的数放到最后面,如果 a[i-1]>a[i],我们将i-1,i的值进行交换,依次循环反复。

🍍实现过程

⭐️用升序举例子首先从下标为1的数开始,依次将前一个数和后一个数交换,之后每一次遍历都会少一个需要遍历的数,单趟冒泡完了,每一个都小于后一个数,那么后面就不需要遍历了

void BubbleSort(int* a, int n) {
    //多趟
    for (int j = 0; j < n; j++) {
        //发生了交换,将flag置1
        int flag = 0;
        //单趟,从1开始,让前一个与后一个比较
        for (int i = 1; i < n - j; ++i) {
            if (a[i] < a[i - 1]) {
                Swap(&a[i], &a[i - 1]);
                flag = 1;
            }
        }
        //没有发生交换,后续不需要再冒泡
        if (flag == 0) {
            break;
        }
    }
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:

第一趟最多比较n-1次,第二趟最多比较n-2次……最后一次最多比较1次,所以总次数是n-1+n-2+……+1,所以说时间复杂度是O(N2)
最好的情况: O(N)(顺序)
最坏的情况: O(N2)(逆序)

⭐️空间复杂度:

O(1),没有开辟额外空间

⭐️稳定性:

冒泡排序在比较遇到相同的数时,不进行交换,这样就保证了稳定性,所以说冒泡排序数稳定的

🍯2.快速排序(递归版本)

🍍hoare版本

🔑基本思想

⭐️用升序举例子:首先定义一个关键字Key(一般是第一个或者是最后一个),然后将数组第一个定义为begin和最后一个定义为endend负责找小,如果遇到endKey小的话,移动begin,找到比Key大的位置,然后交换beginend,如果beginend相遇则将相遇的位置与Key交换。

⭐️原则:关键词取左,右边先找小再左边找大;关键词取右,左边先找大再右边找小。可以保证相遇位置比Key

⭐️一趟单趟排序排完后,此时keyi下标对应的数不用变了,接下来将keyi左边和右边分别分治递归即可,直至左右两边都有序

🔑实现过程

⭐️hoare版本找keyi值代码,注意如果leftright都一样时,rightleft也要移动,不忍会死循环,如果是顺序的数组,要判断下标防止越界

//horae
int PartSort1(int* a, int left, int right) {
    //将第一个值作为keyi
    int keyi = left;
    while (left < right) {
        //(5,5,2,3,5)
        //没有等于的问题是,如果left下标对应的值和right下标对应的值和keyi下标对应的值相等,则会死循环
        //(1,2,3,4,5)
        //如果没有判断left和right,数组是升序的话,right和left会一直访问直到越界
        //注意险些left < right 不然会越界
        while (left < right && a[right] >= a[keyi]) {
            right--;
        }
        while (left > right && a[right] <= a[keyi]) {
            left++;
        }
        Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[keyi]);
    //返回数组第一个元素下标,返回的是left 不是keyi
    return left;
}

⭐️快排代码,找出keyi下标,此时不变,将keyi下标分成左右两个区间,依次递归

void QuickSort(int* a, int left, int right) {
    //此时区间不可以再分割了,此时区间不存在 
    if (left >= right) {
        return;
    }
    int keyi = PartSort1(a, left, right);
    //[left, keyi-1] keyi [keyi+1, right]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
}

🍍挖坑法

🔑基本思想

⭐️相较于horae法,挖坑发不需要理解为什么,最终相遇的位置比key小,不需要理解为什么左边作为key要右边先走.

⭐️选出第一个数或者最后一个数作为坑位储存起来,包括下标,然后然后从右往左找到比key小的数字,将key替换为此数,然后从左往右找到比key大的数字,然后替换为此数,循环往复,直到rightleft相等

🔑实现过程

⭐️将a[left]存到key中,将left下标存到pit中,然后先移动right,交换坑位,之后移动left交换坑位,最后结束循环放入坑位

//挖坑法
int PartSort2(int* a, int left, int right) {
    int key = a[left];
    //坑位
    int pit = left;
    while (right > left) {
        //(5,5,2,3,5)
        //没有等于的问题是,如果left下标对应的值和right下标对应的值和keyi下标对应的值相等,则会死循环
        //(1,2,3,4,5)
        //如果没有判断left和right,数组是升序的话,right和left会一直访问直到越界
        while (right > left && a[right] >= key) {
            right--;
        }
        a[pit] = a[right];
        pit = right;
        while (right > left && a[left] <= key) {
            left++;
        }
        a[pit] = a[left];
        pit = left;
    }
    a[pit] = key;
    return pit;
}

⭐️全趟递归

void QuickSort(int* a, int left, int right) {
    //此时区间不可以再分割了,此时区间不存在 
    if (left >= right) {
        return;
    }
    int keyi = PartSort2(a, left, right);
    //[left, keyi-1] keyi [keyi+1, right]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
}

🍍前后指针法

🔑基本思想

⭐️从左边开始,选择第一个数作为keyi,那么pre从下标为0开始,cur从下标为1开始,直到cur到最后一个数结束,cur在前面找小,找到了,prev往前走一步,然后交换precur所在位置的值,然后cur继续找小,直到cur走到空指针的位置就结束,最后将pre的值与key交换就完成了一次分割区间的操作

⭐️如果选择右边作为keyi的话,那么pre起始下标从-1位置开始,cur从0位置开始,循环在right-1位置结束

🔑实现过程

⭐️用第一个数为keyi为例pre为第一个数下标,cur为第二个数下标,cur一直往后走,直到到right结束,当以cur为下标的值遇到以keyi为下标的值要小,则将pre往后移动一位,同时判断pre如果不等于cur则将cur的值与pre的值交换,之后cur往后走,循环结束后,交换pre下标的值和keyi下标的值,并且返回作为头的pre

//前后指针法
int PartSort3(int* a, int left, int right) {
    int keyi = left;
    int pre = left;
    int cur = left + 1;
    //cur走到尾就结束了,需要等于不然最后一组没有测到
    while (cur <= right) {
        //首先往后找比keyi要小的数
        //找到了比keyi小的数就先++pre
        //如果pre和cur相等就不交换两个数,如果不相等就交换两个数,防止自己和自己交换
        if (a[cur] < a[keyi] && a[++pre] != a[cur]) {
            Swap(&a[cur], &a[pre]);
        }
        //cur接着往下走
        cur++;
    }
    //交换后,此时pre左边比keyi小,右边比keyi大
    Swap(&a[pre],&a[keyi]);
    //返回值给keyi
    return pre;
}

⭐️全趟递归

void QuickSort(int* a, int left, int right) {
    //此时区间不可以再分割了,此时区间不存在 
    if (left >= right) {
        return;
    }
    int keyi = PartSort3(a, left, right);
    //[left, keyi-1] keyi [keyi+1, right]
    QuickSort(a, left, keyi - 1);
    QuickSort(a, keyi + 1, right);
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:
O ( N l o g N ) O(NlogN) O(NlogN)
最好情况每次都选的是中间为key

最坏情况是每次选的keyi都是第一个或者是最后一个,那么每次循环都要遍历所有的数**(此时可能会导致栈溢出)**

⭐️空间复杂度:

空间复杂度一般为为O(logN),最坏情况下是O(N),需要进行n‐1递归调用,退化为冒泡排序

⭐️稳定性分析:

黑色key 2 要放到中间去,有点不稳==不稳定==

🍍优化快速排序

🔑选出中间值优化
🌰基本思想和代码实现

⭐️选出不是最大或者最小的函数,因为遇到的数组是随机的,可能有序可能无序,如果是有序,那么三数取中后时间复杂度从最坏变到最好,随机的情况,即最坏的情况也被避免掉了选出数组中间值的下标,然后每次单趟排序时,将中间的值和第一个数交换,下面是选出中间值代码。

int MidIndex(int* a, int left, int right)
{
    	
        //防止数据溢出
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
}

⭐️此时的单趟排序中改变的只是,中间值和第一个数交换了而已

int PartSort1(int* a, int left, int right)
{
  	  //选出中间值下标,然后交换中间值和第一个数
	int midi = MidIndex(a, left, right);
	Swap(&a[left], &a[midi]);

	//最左边的做key为例
	int key = left;
	while (left<right)
	{
		//因为我们是最左边的取key,所以必须是右边先走找比key小的,思考下为什么?
		//右边先走
		while (left < right && a[right] >= a[key])
		{
			--right;
		}
		//然后左边走
		while (left < right && a[left] < a[key])
		{
			++left;
		}
		Swap(&a[left], &a[right]);
	}
	Swap(&a[left], &a[key]);//此时left已经和right相遇,一样的
	return left;
}

🔑小区间优化
🌰基本思想和代码实现

⭐️在多次排序中,会多次递归调用,最后区间会被切成很小的一块,但其实,当区间变得很小的时候再去递归效率就会显得很慢,所以我们可以选择其他排序来解决这个问题。

image-20220425114542542

⭐️还有一个我们要思考的问题就是最后这段小区间用什么排序比较好?
希尔排序适应的是比较多的数据才有优势,堆排序需要建堆,其他三个插入排序、选择排序和冒泡排序相比,还是插入排序比较优,所以我们小区间选择用插入排序进行排序。

void QuickSort2(int* a, int begin, int end)
{
	// 子区间相等只有一个值或者不存在那么就是递归结束的子问题
	if (begin >= end)
		return;

	// 当区间个数小于10时为小区间,小区间直接插入排序控制有序
	if (end - begin + 1  <= 10)
	{
        	//确定任意位置的小区间
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int keyi = PartSort3(a, begin, end);
		// [begin, keyi-1] keyi [keyi+1, end]
		QuickSort2(a, begin, keyi - 1);
		QuickSort2(a, keyi + 1, end);
	}
}

🍯3.快速排序(非递归版本)

🍍基本思想

⭐️原因:当递归深度过大,可能会造成栈溢出。

⭐️利用栈,首先将要插入的区间头尾放到栈当中,然后循环取出头尾,得到keyi再将[left, keyi-1]和 [keyi+1,right]依次放到栈当中,直到栈为空停止。

🍍实现过程

⭐️首先创建一个栈,然后把我们传过来的区间放到栈当中,然后取出栈当中区间的两头,同时单趟排一遍得到keyi此时keyi左边比keyi小,右边比keyi大,然后判断左右两个区间的边界范围,如果边界相等说明只有一个元素了,就不用再入栈了,依次循环直到栈为空。

void QuickSort2(int* a, int left, int right) {
    //创建并且初始化st然后将
    ST st;
    StackInit(&st);
    //将最开始的区间插入到栈当中
    StackPush(&st, left);
    StackPush(&st, right);
    //栈为空,表示排完了
    while (!StackEmpty(&st)) {
        //取出我们要排序的区间头和尾,注意栈是先入后出
        int right = StackTop(&st);
        StackPop(&st);
        int left = StackTop(&st);
        StackPop(&st);
        //单趟排序得到keyi
        int keyi = PartSort3(a, left, right);
        //[left, keyi-1] keyi [keyi+1, right]
        //当区间越界了就不会再入栈了,说明排到底了
        if (left < keyi - 1) {
            StackPush(&st, left);
            StackPush(&st, keyi - 1);
        }
        if (keyi + 1 < right) {
            StackPush(&st, keyi + 1);
            StackPush(&st, right);
        }
    }
    StackDestory(&st);
}

🌏五.归并排序

🍯1.递归实现

🍍相较于其他排序的优势

⭐️前面的排序都是内排序,数据在内存,访问速度快,但是访问量小,下标随机访问,归并排序是外排序,数据在磁盘,访问速度漫,但是访问量大,串行访问。

🍍基本思想

⭐️该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。

⭐️其实就是先分治再递归,分成若干个小区间然后再合并。

image-20220425172801752

🍍实现过程

⭐️创建一个临时数组,然后将需要排序的数组和临时数组都传送到子函数当中,首先分治:如果只剩下一个元素或者第一个元素下标超出最后一个元素下标时,返回否则获得中间节点,然后将中间节点的左区间和右区间分别分治。然后归并:将两个区间一起比较,将较小的值放入tmp中,之后将未排序完的数组放入到tmp中,再把tmp拷贝到a

void _MergeSort(int* a, int left, int right, int* tmp) {
    //当区间只剩下一个值(=)或者超出区间(>)的时候,结束返回
    if (left >= right) {
        return;
    } 
    //得到中间节点
    int mid = left + (right - left) / 2;
    //分治[left, mid][mid+1, right]
    //最好不要[left, mid-1][mid, right],容易左右不均匀
    _MergeSort(a, left, mid, tmp);
    _MergeSort(a, mid+1, right, tmp);
    //归并[left, mid][mid+1, right]
    //printf("归并[%d,%d][%d,%d]\n", left, mid, mid+1, right);
    //两组left和right分别决定归并的左右两组,两边分别让它们有序
    int left1 = left, right1 = mid;
    int left2 = mid + 1, right2 = right;
    //记录tmp下标的
    int index = left;
    //归并过程是,有一组结束了循环结束
    while (left1 < right1 && left2 < right2) {
        //升序:将两个区间中较小的一个放到tmp中
        if (a[left1] < a[left2]) {
            tmp[index++] = a[left1++];
        }
        else {
            tmp[index++] = a[left2++];
        }
    }
    //检查两个区间中剩下的数归并到tmp中
    while (left1 <= right1) {
        tmp[index++] = a[left1++];
    }
    while (left2 <= right2) {
        tmp[index++] = a[left2++];
    }
    //拷贝数据从tmp到a,闭区间个数要加1
    //+left:每次拷贝不是考全部,只需要拷贝归并的那一段就可以了
    memcpy(a+left, tmp+left,(right-left + 1)*sizeof(int));
}

void MergeSort(int* a, int n) {
    //创建一个临时数组
    int* tmp = (int*)malloc(sizeof(int) * n);
    assert(tmp);
    _MergeSort(a, 0, n - 1, tmp);
    free(tmp);
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:

O ( N l o g N ) O(NlogN) O(NlogN)
递归过程中每次都将一组平均分,分完后高度大概是logN

⭐️空间复杂度:
O ( N ) O(N) O(N)
要来一个临时空间存放归并好的区间的数据

⭐️稳定性分析:

在遇到相同的数时,可以就先将放前一段区间的数,再放后一段区间的数就可以保持稳定性了,所以说这个排序是稳定的.

🍯2.非递归实现

🍍基本思想

⭐️首先区间的间距是1,所以就以间距为1一组一组合并,然后区间间距是2,所以就以间距是2一组一组合并,以此类推,直到所有区间都合并了。

image-20220426151318810

🍍实现过程

⭐️首先创建一个临时数组tmp,一开始gap间距是1,饭后我们定义左区间是[i, i+gap-1],右区间是[i+gap, i+2*gap-1],每一次循环到n-1为止,间隔是跳过两个区间,如果遇到right1越界则修正,left2越界则表示区间不存在,left2正常right2越界则修正right2,接着就是归并的过程,例如升序就将两个区间中较小的值放入tmp中,直到直到两个区间中有一个没有数据了结束,之后检查两个区间中剩下的数归并到tmp中,然后将tmp中的数拷贝到a中。

image-20220426153427094

void MergeSortNot(int* a, int n) {
    int* tmp = (int*)malloc(sizeof(int) * n);
    assert(tmp);
    int gap = 1;
    //当间距小于数组长度时继续
    while (gap < n) {
        for (int i = 0; i < n; i += gap * 2) {
            //控制两个区间的边界
            //[i, i+gap-1] [i+gap, i+2*gap-1]
            int left1 = i, right1 = i + gap - 1;
            int left2 = i + gap, right2 = i + gap * 2 - 1;
            int index = i;
            // 情况1  right1越界 修正
            if (right1 >= n)
                right1 = n - 1;
            // 情况2  left2越界 表示第二个区间不存在,修正成一个不存在的区间
            if (left2 >= n) {
                left2 = n;
                right2 = n - 1;
            }
            // 情况3 left2正常 right2越界 修正right2
            if (left2 < n && right2 >= n)
                right2 = n - 1;
            printf("归并[%d,%d][%d,%d]--gap = %d\n", left1, right1, left2, right2,gap);
            //归并过程是,有一组结束了循环结束
            while (left1 <= right1 && left2 <= right2) {
                //升序:将两个区间中较小的一个放到tmp中
                if (a[left1] < a[left2]) {
                    tmp[index++] = a[left1++];
                }
                else {
                    tmp[index++] = a[left2++];
                }
            }
            //检查两个区间中剩下的数归并到tmp中
            while (left1 <= right1) {
                tmp[index++] = a[left1++];
            }
            while (left2 <= right2) {
                tmp[index++] = a[left2++];
            }
        }
        //拷贝数据从tmp到a,闭区间个数要加1
        //+left:每次拷贝不是考全部,只需要拷贝归并的那一段就可以了
        memcpy(a, tmp, n * sizeof(int));
        //每次gap间距都乘以2
        gap *= 2;
    }
    free(tmp);
    tmp = NULL;
}

🌏六.计数排序(非比较排序)

🍍基本思想

⭐️计数排序是一个非基于比较的排序算法,优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序。基数排序可以排序负数,但是不能排序浮点数

🍍实现过程

⭐️为了不必要的空间浪费,我们首先采用相对映射的方法,计算出数组中的最大值和最小值,然后依据最大值和最小值开辟一个空间count,然后将所有数组映射到count中,之后偏离数组将所有数组依次取出来

void CountSort(int* a, int n) {
    //初始化min和max
    int min = a[0];
    int max = 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*)calloc(range, sizeof(int));
    assert(count);
    //遍历映射计数
    for (int i = 0; i < n; ++i) {
        count[a[i] - min]++;
    }
    //遍历排序数组
    int index = 0;
    for (int i = 0; i < range; ++i) {
        while (count[i]--) {
            a[index++] = i + min;
        }
    }
    free(count);
    count = NULL;
}

🍍时间复杂度、空间复杂度、稳定性分析

⭐️时间复杂度:

O ( N + K ) O(N+K) O(N+K)
取决于次数和数组范围谁更大

⭐️空间复杂度:
O ( N ) O(N) O(N)
⭐️稳定性分析:

计数是在统计每个数出现的次数,但是相同的数哪个在前哪个在后,并没有区分,所以我们写的不稳定.

但是只是我们这里写的不稳定,计数排序可以写成稳定的,所以综合来说是稳定的

🌏七.八大排序比较

🍯1.性能测试代码

#define N 10000

void TestOP()
{
	srand((unsigned int)time(NULL));

	int* a1 = (int*)malloc(sizeof(int) * N);
	int* a2 = (int*)malloc(sizeof(int) * N);
	int* a3 = (int*)malloc(sizeof(int) * N);
	int* a4 = (int*)malloc(sizeof(int) * N);
	int* a5 = (int*)malloc(sizeof(int) * N);
	int* a6 = (int*)malloc(sizeof(int) * N);
	int* a7 = (int*)malloc(sizeof(int) * N);
	int* a8 = (int*)malloc(sizeof(int) * N);

	int i = 0;
	for (i = 0; i < N; i++)
	{
		a1[i] = rand();
		a2[i] = a1[i];
		a3[i] = a2[i];
		a4[i] = a3[i];
		a5[i] = a4[i];
		a6[i] = a5[i];
		a7[i] = a6[i];
		a8[i] = a7[i];
	}

	int begin1 = clock();
	//InsertSort(a1, N);
	int end1 = clock();

	int begin2 = clock();
	//ShellSort(a2, N);
	int end2 = clock();

	int begin3 = clock();
	//SelectSort(a3, N);
	int end3 = clock();

	int begin4 = clock();
	//HeapSort(a4, N);
	int end4 = clock();

	int begin5 = clock();
	//QuickSort(a5, 0, N - 1);
	int end5 = clock();

	int begin6 = clock();
	//MergeSort(a6, N);

	int end6 = clock();

	int begin7 = clock();
	//BubbleSort(a7, N);
	int end7 = clock();

	int begin8 = clock();
	//CountSort(a7, N);
	int end8 = clock();

	printf("InsertSort:%dms\n", end1 - begin1);
	printf("ShellSort:%dms\n", end2 - begin2);
	printf("SelectSort:%dms\n", end3 - begin3);
	printf("HeapSort:%dms\n", end4 - begin4);
	printf("QuickSort:%dms\n", end5 - begin5);
	printf("MergeSort:%dms\n", end6 - begin6);
	printf("BubbleSort:%dms\n", end7 - begin7);
	printf("CountSort:%dms\n", end8 - begin8);

	free(a1);
	free(a2);
	free(a3);
	free(a4);
	free(a5);
	free(a6);
	free(a7);
	free(a8);
}


void TextInsertSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	InsertSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}

void TextBubbleSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	BubbleSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}

void TextShellSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	ShellSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextHeapSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextSelectSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	SelectSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextQuickSort() {
	int a[] = { 9,1,2,5,7,4,8,6,3,5 };
	QuickSort2(a, 0, sizeof(a) / sizeof(a[0])-1);
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextMergeSort() {
	int a[] = { 10,6,7,1,3,9,4,2,5,2 };
	PrintArry(a, sizeof(a) / sizeof(a[0]));
	MergeSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextMergeSortNot() {
	int a[] = { 4,5,2,9,3,4,7,5,8 };
	PrintArry(a, sizeof(a) / sizeof(a[0]));
	MergeSortNot(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}
void TextCountSort() {
	int a[] = { 4,5,2,9,3,4,7,5,8 };
	//PrintArry(a, sizeof(a) / sizeof(a[0]));
	CountSort(a, sizeof(a) / sizeof(a[0]));
	PrintArry(a, sizeof(a) / sizeof(a[0]));
}

🍯2.排序空间复杂度、时间复杂度、稳定性

⭐️稳定性:相同的数排完数之后,相对顺序不变,那么就是稳定的。

排序方法时间复杂度(平均情况)时间复杂度(最好)时间复杂度(最坏)空间复杂度稳定性
插入排序O(N2)O(N)O(N2)O(1)稳定
希尔排序O(N1.3)O(N)O(N2)O(1)不稳定
选择排序O(N2)O(N2)O(N2)O(1)不稳定
堆排序O(NlogN)O(NlogN)O(NlogN)O(1)不稳定
冒泡排序O(N2)O(N)O(N2 )O(1)稳定
快速排序O(NlogN)O(NlogN)O(N2 )O(logN)不稳定
归并排序O(NlogN)O(NlogN)O(NlogN)O(N)稳定
计数排序O(N+K)(k是整数的范围)O(N+K)O(N+K)O(N+K)稳定

🌏总结

⭐️排序当然不止这八种,过不这几种比较经典,感谢阅读。

⭐️码字不易喜欢的话,欢迎大家点赞支持和指正~

  • 56
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 53
    评论
评论 53
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penguin_bark

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值