数据结构--排序

1. 排序的概念

  • 排序:所谓排序,就是一串记录,按照其他的某个或某些关键字的大小,递增或递减的排列起来的操作
  • 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]==r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称之为不稳定
  • 内部排序:数据元素全部放在内存中的排序
  • 外部排序:数据元素太多,不能同时放在内存中,根据排序过程的要求不能再内存之间移动数据的排序
    在这里插入图片描述

2. 排序

详细的各种排序过程可以参考下面这个网站
https://visualgo.net/en/sorting?slide=6

2.1. 插入排序

插入排序的思想:从第2个元素开始标记,别标记的数每次与前面紧挨着的数据进行比较,只要比前面的数据小,就与前面的数据交换,直到紧挨的数据大于被标记的数据.

  • 第1次标记第2个元素5
    在这里插入图片描述
    5与第一个元素6比较,小于6,互换位置
    在这里插入图片描述

  • 第2次标记第3个元素4
    在这里插入图片描述4与6相比,小于6,互换
    4与5比较,小于5,互换
    在这里插入图片描述

  • 第3次标记第4个元素7
    在这里插入图片描述
    7与6相比,大于6,不操作
    在这里插入图片描述

  • 第4次标记第5个元素8
    在这里插入图片描述
    8与7相比,大于7,不操作
    在这里插入图片描述

  • 第5次标记第6个元素3
    在这里插入图片描述
    3与8相比,小于8,互换
    3与7相比,小于7,互换
    3与6相比,小于6,互换

    3与4相比,小于4,互换
    在这里插入图片描述

    通过每次标记值,与前面的数据比较,不断使被标记前的数据达到有序

代码实现:

void InsertSort(int* a,int n)
{
	for(int i=0;i<n-1;i++)
	{
		//tmp是被标记的数字,end是与被标记数字的下标
		int end=i;
		int tmp=a[end+1];
		while(end>=0)
		{
			if(a[end]>tmp)
			{
				//出现小于的情况,用大的数覆盖被标记的数
				a[end+1]=a[end];
				end--;
			}
			else
			{
			//没有小于的情况说明标记的数字位置已经合适了,直接退出循环
				break;
			}
		}
		a[end+1]=tmp;
		//tmp保存了之前被标记的数,将tmp的数放在正确的位置
	}
}

在这里插入图片描述

时间复杂度:
最好的情况下:O(N)
最坏的情况下:O(N^2)
空间复杂度:O(1)

2.2.希尔排序

2.2.1. 希尔排序的思想

希尔排序的主要步骤:

  1. 预排序
  2. 直接插入排序
    预排序:分组排,间隔为gap的数据分为一组,gap大于1
    在这里插入图片描述
    分组:gap=3的时候
    在这里插入图片描述
    9,5,8,5为一组。
    1,7,6为一组。
    2,4,3为一组。
    然后分别对三组进行预排序(直接插入排序)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    与最开始的顺序相比,稍微有序了一点,大的数据靠后点,小的数据靠前点。
    如果先用gap=5处理,再用gap=3处理,数据会越来越有序
    在这里插入图片描述
    当gap=1的时候,就是直接插入排序
    在这里插入图片描述
    注:在这里插入图片描述
    没有预排序的情况下,处于第一元素位置的9想要去后面需要移动9次
    但是前几次的预排序会快速将9移到后面,使9想要进入有序的过程中,只需要移动几次就可以了,如gap=3的情况下,只需要移动3次

2.2.2.代码实现

对一组数据进行排序

void Shell(int*a,int n)
{
    int gap=3;
    //按每隔3个数为一组,将所有数据分为三粗
    //下面就是对每组进行直接插入排序
    for(int i=0;i<n-gap;i+=gap)
    {
        int end=i;
        int tmp=a[end+gap];
        while(end>=0)
        {
            if(tmp<a[end])
            {
                a[end+gap]=a[end];
                end-=gap;
            }
            else
            {
                break;
            }
        }
        a[end+gap]=tmp;
    }
}

对所有组进行预排序

void Shell(int*a,int n)
{
    int gap=3;
    //一共三组,每组进行一次排序
    for(int j=0;i<gap;j++)
    {
        for(int i=0;i<n-gap;i+=gap)
        {
            int end=i;
            int tmp=a[end+gap];
            while(end>=0)
            {
                if(tmp<a[end])
                {
                    a[end+gap]=a[end];
                    end-=gap;
                }
                else
                {
                    break;
                }
            }
            a[end+gap]=tmp;
        }
    }
}

简化

void Shell(int*a,int n)
{
void ShellSort(int*a,int n)
{
    int gap=n;
    while(gap>1)
    {
        gap/=2;
        //gap越分越小,直到为1
        for(int i=0;i<n-gap;i++)
        {
            int end=i;
            int tmp=a[end+gap];
            while(end>=0)
            {
                if(tmp<a[end])
                {
                    a[end+gap]=a[end];
                    end-=gap;
                }
                else
                {
                break;
                }
            }
            a[end+gap]=tmp;
        }
    }
}

gap/2的情况下,预排序较多
可以使用gap/3+1,但是gap/3最后的结果不一定是1(8/3=2),所以,我们可以在后面加一,(gap/3+1),这样能保证最后一次的结果也是1

2.2.3.希尔与直接插入的对比

希尔排序经历过那么多次的预排序,时间真的会比直接插入少吗
我们可以验证一下
用下面的函数判断两个时间长短
(ps:在release下运行会更快)

void TestOP()
{
        srand(time(0));
        const int N = 100000;
        int* a1 = (int*)malloc(sizeof(int)*N);
        int* a2 = (int*)malloc(sizeof(int)*N);
        for (int i = 0; i < N; i++)
        {
                a1[i] = rand();
                a2[i] = a1[i];
        }
		
        int begin1 = clock();
        //InsertSort(a1, N);
        int end1 = clock();

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

        printf("InsertSort:%d\n", end1 - begin1);
        printf("ShellSort:%d\n", end2 - begin2);
}

1万个数据的情况下
1w哥数据的情况下
10万个数据的情况下
10w个数据的情况下
100万个数据下,插入排序跑崩了
但是希尔在100万和1000万下还是很猛
100万数据的情况下
在这里插入图片描述
1000万数据的情况下
在这里插入图片描述

2.2.4.希尔排序的时间复杂度

  1. gap很大的时候,如n/3,整个数组逆序,n/3组数据
    最坏的情况下,每组比较3次
    单趟排序合计n/3*3=n次
  2. gap很大的时候,如n/9,整个数组逆序,n/9组数据
    最坏的情况下,每组比较9个数据
    单组合计需要(8+1)8/2=36次
    单趟排序合计n/9
    36=4*n
  3. gap很小的时候,如1,这个时候数组接近有序,单趟排序合计:n

在这里插入图片描述
结论:O(N^1.3)
(通过大量实验后得到的结论,反正按照我的数学水平想算出来很难)
比N*logN略差,但是对于排序来说已经很牛逼了

2.3. 冒泡排序

冒泡排序是最开始接触c语言时就使用的排序
这里简单提一下
冒泡排序是将最大的值一步一步交换到数组的最后一个位置

  • 第一次循环:
    第1个元素和第2个元素比,大的元素放2号元素位
    第2个元素和第3个元素比,大的元素放3号元素位
    第3个元素和第4个元素比,大的元素放4号元素位

    第n-1个元素和第n个元素比,大的元素放n号位
    第一次循环结束,能保证最大的元素处于最后一个n的位置
  • 第二次循环
    从第1个元素和第2个元素开始,大的放2号位

    第n-2个元素和第n-1个元素比,大的放n-1号位
    第二次循环结束,能保证第二大的元素在n-1的位置
  • 一直到只剩下1号位后面的元素排好序,冒泡排序算完成
    代码实现:
void Swap(int*a,int *b)
{
	int tmp=*a;
	*a=*b;
	*b=tmp;
}

void BubbleSort(int*a,int n)
{
	for(int i=0;i<n;i++)//控制总的循环次数
	{
		for(int j=1;j<n-i-1;j++)//
		{
			if(a[j-1]>a[j])
			{
				Swap(&a[j-1],&a[j]);
			}
		}
	}
}

冒泡排序还有可以有优化的地方,如果在某次从第一个元素到m(m<=n)个元素都已经有序了,冒泡还会进入下一次循环,从第一个遍历到最后m-1个,但是这些数据还是有序的,依旧不会改变顺序,
我们可以设置一个变量,当某次排序没有发生交换,则说明后面已经有序,需需要进入下次循环


void BubbleSort(int*a,int n)
{
	for(int i=0;i<n;i++)//控制总的循环次数
	{
		int flag=1;
		for(int j=1;j<n-i;j++)
		{
			if(a[j-1]>a[j])
			{
				Swap(&a[j-1],&a[j]);
				flag=0;
			}
		}
		if(flag==1)
		{
			break;
		}
	}
}

在这里插入图片描述

时间复杂度:O(N^2);

2.4. 选择排序

选择排序在每次遍历数组的时候,将这次遍历中最大的数和最小的数的下标标记,最小的数和下标为begin互换,最大的数和下标为end的数互换
在这里插入图片描述
第一次遍历
mini将最小的数字2的下标标记
maxi将最大的数字8的下标标记
在这里插入图片描述

然后将maxi下标的数字与end位置互换
mini下标的数字与begin位置互换
在这里插入图片描述
此时begin++,end–,再将maxi和mini的下标都重置为begin
在这里插入图片描述
进入第二次遍历:

maxi找最大,mini找最小
在这里插入图片描述
然后将begin和mini的值互换,end与maxi的值
在这里插入图片描述
后面的思路一致,不过进行的条件是begin<end,如果条件不满足,begin前和end后面的序已经排好,两个相遇,整个数组的序已经排好,可以结束
代码实现:

void SelectSort(int*a,int n)
{
	int begin=0,end=n-1;
	while(begin<end)
	{
		int maxi=begin,mini=begin;
		for(int i=begin+1;i<=end;i++)
		{
			if(a[i]>a[maxi])
			{
				maxi=i;
			}
			if(a[i]<a[mini])
			{
					mini=i;
			}
		}
		Swap(&a[begin],&a[mini]);
		Swap(&a[end],&a[maxi]);
		end--;
		begin++;
	}
}

在这里插入图片描述
虽然理论看似没问题,但是结果出现了问题,3排在倒数第4位
我们用下面的数据举例解释这种情况
在这里插入图片描述
如果maxi此时指向的就是从begin到end中最大的数字
在这里插入图片描述
首先mini的数字会与begin下标的数字发生交换
在这里插入图片描述
交换以后,maxi指向的不是最大的,但是因为这是在改变maxi之后发生的操作,maxi此后会与end的位置交换,这个时候交换过去的就不是这串数中的最大值,而在交换之后,end–,我们就会默认后面已经排好序,也就不回去管那个数字是否在正确的位置
我们可以加个判断,当maxi的值还是begin的时候,begin会和mini交换,此时最大的数字交换到mini的位置上,这个时候我们就让mini位置的数字与end交换即可

void SelectSort(int*a,int n)
{
	int begin=0,end=n-1;
	while(begin<end)
	{
		int maxi=begin,mini=begin;
		for(int i=begin+1;i<=end;i++)
		{
			if(a[i]>a[maxi])
			{
				maxi=i;
			}
			if(a[i]<a[mini])
			{
					mini=i;
			}
		}
		Swap(&a[begin],&a[mini]);
		if(maxi==begin)
		{
			maxi=mini;
		}
		Swap(&a[end],&a[maxi]);
		end--;
		begin++;
	}
}

在这里插入图片描述
这个时候的结构没问题

2.5.快速排序

  • 快速排序是Hoare(霍尔)在1962年提出的一种二叉树结构的交换顺序
  • 基本思想:任取待排序元素序列中的某些元素作为基准值,按照该排序码将排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有的元素都安排到对应位置上为止

2.5.1hoare版本

这个方法是最开始Hoare想出的方法
实现思路:

  • 确定数组中首个元素作为key,然后将所有小于key的元素放在key左边,所有大于key的元素放在key的右边
    在这里插入图片描述

  • R从数组右边开始走,当R走到小于key的元素时,R不动,L从数组的左侧出发,找大于key的元素
    在这里插入图片描述
    在这里插入图片描述

  • 当L为找到大于key的值,R为小于key的值,L和R的值分别进行交换

    -此时R接着往左边走,找小于key的值,L往右走,找大于key的值(R先走,L后走)
    在这里插入图片描述
    在这里插入图片描述

  • 接着互换位置
    在这里插入图片描述

  • R和L接着走,当相遇的时候停下
    在这里插入图片描述

  • 此时,将key与L和R相遇位置的数字3与key的值6交换
    在这里插入图片描述第一次循环数组,我们将所有小于key的元素放在了key左边,大于key的元素放在了右边,然后我们从key的位置将数组分为左右子序列
    第二层处理:

  • 左边数组:
    在这里插入图片描述
    与上面类似,先找将些数的第一个元素作为key
    R从右边走找小,L从左边走找大(R先走,L后走)
    在这里插入图片描述在这里插入图片描述
    L和R相遇停止,将2和3位置互换
    在这里插入图片描述

  • 右边数组:
    在这里插入图片描述
    找key,R从右走找小,L从左走找大(R先走,L后走)
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    第二层左右数组处理完毕,然后从两个key的位置,再将左右数组分别分成两个数组
    第三次处理:

  • 第二层左数组的左数组:
    在这里插入图片描述
    将2作为新的key,R从右边1开始找,L从左边2开始找(R先走,L后走)
    在这里插入图片描述
    相遇,互换
    在这里插入图片描述

  • 第二层左数组的右数组:
    在这里插入图片描述
    key=5,R右找小,L左找大(R先走,L后走)
    在这里插入图片描述
    在这里插入图片描述

  • 第二层右数组的左数组
    在这里插入图片描述
    阿巴阿巴(R先走,L后走)
    在这里插入图片描述
    在这里插入图片描述

  • 第二层右数组的右数组

  • 在这里插入图片描述
    只有一个数比不了,直接返回

结果:
排完序:1 2 3 4 5 6 7 8 9 10

2.5.2代码实现hoare版本

  • 第一次找key进行操作
int PartSort(int*a,int left,int right)//right是下标,传入的参数为sz-1
{
	int key=a[left];
	while(left<right)
	{
		if(a[right]>key)//R找小,R大于key的时候R--,小于跳出循环,保存下标
		{
				right--;
    	 }
    	 if(a[left]<key)
    	 {
    	 		left++;
    	 }
    	 Swap(&a[left],&a[right]);//这里的交换函数只有聪明人才能看见(doge)
    }
    
    Swap(&a[left],&key);
    return left;
}

在这里插入图片描述
理论上好像没问题,但是程序好像死循环了呢
返回程序修改
当L和R都是和key的值相同的时候,P不会往左走,L也不会向右走,会进入死循环
所以在判断条件要改成

if(la[right]>=key)

 if(a[left]<=key)

但是修改以后不循环了,但是还有问题
在这里插入图片描述
接着修改,我们的key定义的是第一个元素的值,但只是有值,与数字中对应的值是两块空间,而我们每次交换的时候,是用key和后面的值进行交换数组中我们确定的那个key对应值的位置我们没有改变
我们可以将key设置为数组下标,每次交换的时候可以与数组key位置交换
将key=left,然后把所有的key换为a[key]

然后我们会惊奇的发现,还错了
在这里插入图片描述
这里还存在数组越界的问题
内存循环中,当R和L相遇的时候,我们只有在外层循环才能判断,内层判断不了,这就导致内存循环R和L相遇的时候,R或L还会接着向左或向右找下去
在这里插入图片描述
在这里插入图片描述
这个时候L和R不相遇,并且已经处理好的数据还被打乱了,我们需要在内层循环也加上判断的条件
修改后:

int PartSort(int* a, int left, int right)//right是下标,传入的参数为sz-1
{
	int key = left;
	while (left < right)
	{
		if (left<right&&a[right] >= a[key])//R找小,R大于key的时候R--,小于跳出循环,保存下标
		{
			right--;
		}
		if (left<right && a[left] <=a[key])
		{
			left++;
		}
		Swap(&a[left], &a[right]);//这里的交换函数只有聪明人才能看见(doge)
	}

	Swap(&a[left], &a[key]);
	return left;
}

在这里插入图片描述
注:
相遇:

  1. R动L不动,和L相遇
    每一轮都是R先走,前一轮R和L互换之后,L的位置是小于key的,此时R向左走,能保证R向左遇见L的时候,遇见地方的值小于key
  2. L动R不动, 和R相遇
    R不动的时候,R此时已经处于小于key的位置,此时L向右走,遇见R的位置的值必定小于key

2.5.3效率及优化

快速排序是配得上他的称号的
在与希尔排序,堆排序的比较下,快排都很强
10w个数据(随机数据)
在这里插入图片描述
100w个数据(随机数据)
在这里插入图片描述
1000w个数据(随机数据)
在这里插入图片描述
在随机数据情况下,快排很强
但是在数组有序的情况下,上面的方法就不太行了
10w个数据(有序)
在这里插入图片描述
100w个数据(有序)
在这里插入图片描述
我们分析一下这个问题
在这里插入图片描述
R会一路找,找到L的位置,此时1左面没有元素,右边n-1个元素会进入右边的递归,然后L,R遍历完n-1个数据,而在右边的递归中,还是会出现这种情况,n-2个元素进入右层递归,然后L,R遍历完n-2个数据,此时时间复杂度大大增加,会变成O(n^2)
在这里插入图片描述
正常情况下
在这里插入图片描述
优化的方法:三树取中
每次在最左边,中间,最右边取值,找大小在中间的数,将它设置为key,这样的话,能保证每次处理完一遍后,key对应的值在数组中间,或者中间附近
代码实现:

int GetMidi(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[right]>a[left])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
    else//left<mid
    {
        if(a[mid]<a[right])
        {
            return mid;
        }
        else if(a[right]<a[left]{
            return left;
        }
        else
        {
            return right;
        }
    }
}

int PartSort(int* a, int left, int right)
{
        int midi = GetMidi(a,left,right);
        Swap(&a[left], &a[midi]);
        int key = left;
        while(left<right)
        {
                while (left < right && a[right] >= a[key])
                {
                        right--;
                }
                while (left < right && a[left] <= a[key])
                {
                        left++;
                }
                Swap(&a[left], &a[right]);
        }
        Swap(&a[key], &a[left]);
        return left;
}

void QuickSort(int* a, int begin,int end)
{
        if (begin >= end)
        {
                return;
        }
        int ret = PartSort(a, begin, end);
        QuickSort(a, begin, ret - 1);
        QuickSort(a, ret+1, end);
}

10w数据(有序)
在这里插入图片描述
20w个数据(有序)
在这里插入图片描述
1亿个数据(有序)
在这里插入图片描述
1亿个数据(乱序)
在这里插入图片描述

快速排序,传入的数据必须是N-1(找bug找了几十分钟,就是这个小东西害的)

2.5.4挖坑法

挖坑法也是依据快排的思想,只不过对每次处理的方法不同
先确顶首元素,用key标记数,此时坑是left位置,R向左找小,找到后,将R位置的小值直接填在坑位置R此时的位置变成坑,L在向左走,找大,找到后,将L位置的大值,填在坑处,L的位置留下一个坑,然后R再向前找小,直到L和R相遇

  • 先确定第一个元素作为key,left位置就变成一个坑,用hole标记
    在这里插入图片描述
    R向左找小
    在这里插入图片描述
    找到小了之后,将小值填到坑里
    在这里插入图片描述
    此时R位置为坑,空hole标记
    在这里插入图片描述
    L向右走找大,遇见大值停止
    在这里插入图片描述
    将L的大值填到hole处,然后用hole标记L位置
    在这里插入图片描述
    R左找小
    在这里插入图片描述
    换到hole处,hole标记R位置,L右找大
    在这里插入图片描述
    阿巴阿巴阿巴
    在这里插入图片描述
    阿巴巴巴阿巴
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    最后相遇的时候,将相遇位置坑用key的值填进去
    在这里插入图片描述
    注:挖坑法排完一次的顺序,可能和中分法结果不一样
    中分:
    在这里插入图片描述
    挖坑:
    在这里插入图片描述

2.5.5代码实现挖坑法

int PartSort(int*a,int left,int right)
{
	int key=a[left];
	int hole=left;
	while(left<right)
	{
		while(left<right&&a[right]>=key)
		{
			right--;
		}
		a[hole]=a[right];
		hole=right;
		while(left<right&&a[left]<key)
		{
			left++;
		}
		a[hole]=a[left];
		hole=left;
	}
	a[hole]=key;
	return hole;
}

2.5.6前后指针

这个相较于前面两个很难理解一点,但是学会了之后,代码量和出错率会比前面两种方法小很多

  1. cur找小,++prev,交换prev和cur的值
  • prev有两种情况

  • 在prev没有遇见比ket大的时候,prev紧跟着cur
    -cur遇到了比key大的时候,prev在比key大的一组值前面

  • 首先prev指向第一个元素,cur指向prev下一个元素,将第一个元素用key标记,前提是cur要小于等于数组中元素个数,这样才有意义
    在这里插入图片描述

  • 如果cur遇见小于key的值,就让prev停下
    在这里插入图片描述

  • 如果此时cur指向的位置小于key,并且prev++!=cur,说明两个不是同一个位置,将cur的小值与prev后面的大值进行交换
    在这里插入图片描述
    cur找到的值大于key,prev不动
    在这里插入图片描述
    cur找到的值小于key,并且prev++不等于cur,将prev++的值与cur的互换
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.5.7代码实现前后指针法

int PartSort(int*a,int left,int right)
{
	int key=left;
	int prev=left;
	int cur=prev+1;
	whhile(cur<=right)
	{
		if(a[cur]<a[key]&&prev++!=cur)
		{
			Swap(&a[prev],&a[cur]);
		}
		cur++;
	}
	Swap(&a[prev],&a[key]);
	return prev;
}

2.5.8. 小区间优化

在这里插入图片描述

  • 快速排序的方法确实快,但是当只有10个数据的时候,还是要进行最少两次的递归,而这种类似二叉树的结构,子节点占所有节点的50%,也就是说有最后会有许多零散的数据还会不停地进入递归,哪怕最后的数据量很少。

  • 在这里插入图片描述

  • 所以当递归到某次,传入的需要调整位置的数据小于等于10个左右的时候,为了减少递归次数,我们可以用其他排序的方法。

  • 这里建议快速排序,在数据量很小的情况下,各个排序差别不大。
    代码实现:

void QuickSort1(int*a,int begin,int end)
{
	if(begin>=end)
	{
		return ;
	}
	//数组数据量大于10,快排
	if(end-begin+1>10)
	{
		int ret=PartSort(a,begin,end);//这里的PartSort是中分法(哪个方法都可以)
		QuickSort1(a,begin,ret-1);
		QuickSort1(a,ret+1,end);
	}
	else//数据量少于等于10,插入排序
	{
		InsertSort(a+begin,end-begin+1);
	}
}

在这里插入图片描述
在保证结果没问题的情况下,我们对比一下效率
100w数据(无序,release)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在debug版本下的效率
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总体来说确实优化了一点(部分情况下会出现优化后没跑过原版,但是出现的次数很少很少)

2.5.9. 非递归快排

想要实现类似于递归的思路,可以使用栈来实现
在这里插入图片描述
注:这里的数字仅仅代表下标,同时设置的keyi是中间的下标,左数组的区间为(0,keyi-1),右边数组的下标是(keyi+1,end)

  • 我们先将(0(begin),9(end))下标入栈

在这里插入图片描述

  • 然后将(0,9)拆成(0, 4)和(6, 9),同时将(0,9)出栈入的时候先入右边数组(6,9)的下标,然后在入左边数组的下标(0,4)
    在这里插入图片描述
    在这里插入图片描述
  • 然后我们再拆,将(0,4)拆成(0,1)和(3,4),将(0,4)出栈的同时,将(3,4),(0,1)按顺序入栈

在这里插入图片描述

  • (0,1)拆开,拆成(0,0)和(2,1)此时将(2, 1)和(0,0)入栈,又因为这个时候begin>=end,下一步又要将(0,0)和(2,1)出栈
    实际上(0,0)(2,1)不进栈,这里为了方便理解
    在这里插入图片描述
  • (0,0)和(2,1)出栈会去处理(3,4),(3,4)可分为(3,3)和(5,4),入栈,begin>=end,又要出栈(3,3)不进栈,这里只是方便理解
    在这里插入图片描述
    这种顺序类似深度遍历,如果使用队列的话类似广度遍历
    栈代码:
#pragma once
#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

#define N 10
//typedef struct stack
//{
//        int arr[N];
//        int top;
//}stack;
typedef int STDdatatype;
typedef struct stack
{
        STDdatatype* a;
        int top;//top 栈顶
        int capacity;
}ST;

void STinit(ST* ps);//初始化
void STDestory(ST* ps);//销毁
void STPush(ST* ps,STDdatatype x);//入栈
void STPop(ST* ps);//出栈
int STSize(ST* ps);//栈元素数量
STDdatatype STTop(ST* ps);//返回栈顶元素
bool STEmpty(ST* ps);//是否为空

void STinit(ST* ps)
{
        assert(ps);
        ps->a = NULL;
        ps->capacity = 0;
        ps->top=0;
}
void STDestory(ST* ps)
{
        assert(ps);

        free(ps->a);
        ps->a = NULL;
        ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDdatatype x)
{
        assert(ps);
        if (ps->top == ps->capacity)
        {
                int newcapacity = ps->capacity == 0 ? 4: ps->capacity * 2;
                STDdatatype* tmp = (STDdatatype*)realloc(ps->a, sizeof(STDdatatype) * newcapacity);
                if (tmp == NULL)
                {
                        perror("realloc fail");
                        exit(-1);
                }
                ps->a = tmp;
                ps->capacity = newcapacity;
        }
        ps->a[ps->top] = x;
        ps->top++;
}
void STPop(ST* ps)
{
        assert(ps);
        assert(ps->top>0);
        --ps->top;
}
STDdatatype STSize(ST* ps)
{
        assert(ps);
        return ps->top;
}
bool STEmpty(ST* ps)
{
        assert(ps);
        return ps->top == 0;
}

STDdatatype STTop(ST* ps)
{
        assert(ps);
        assert(ps->top > 0);
        return ps->a[ps->top-1];
}
void QuickSortNonR(int*a,int begin, int end)
{
	ST st;
	STinit(&st);
	STPush(&st,end);
	STPush(&st,begin);
	
	while(!STEmpty(&st))
	{
	    //取栈顶小的下标
		int left=STTop(&st);
		STPop(&st);
		
		int right=STTop(&st);
		STPop(&st);
		//确定中间位置的元素
		int keyi=PartSort(a,left,right);
		
		if(keyi+1<right)
		{
			STPush(&st,right);
			STPush(&st,keyi+1);
		}
		if(keyi-1>left)
		{
			STPush(&st,keyi-1);
			STPush(&st,left);
		}
	}
STDestory(&st);
}

在这里插入图片描述

请添加图片描述

2.6. 归并排序

2.6.1归并思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是次用分治法(Divide and Conquer)的一个非常典型的应用。将已有的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序,若将两个有序表合并成一个有序表,称为二路归并

以上都是概念
归并排序,就是将两个有序的子序列,按照大小连接起来的过程,要使子序列有序,可以把子序列用两个它的有序子序列连接,依次类推,直到子序列只含有一个元素的时候向上返回
在这里插入图片描述
请添加图片描述
运用的方法是有序数组合并,不过只能在新开辟的数组中操作,在新开辟的数组中填入值,只需要判断a左右子序列元素的大小
但是如果在原数组中操作,既要比较两个子序列数组元素的大小,同时还要在里面覆盖数据,很容易混乱
所以归并排序最好或者只能开辟新数组储存

2.6.2递归代码实现

注:这里每个子序列排完序就会将对应区间覆盖在a数组对应的区间

void _MargeSort(int *a,int*tmp,int begin,int end)
{
	if(begin>=end)
	{
		return ;
	}
	int mid=(left+right)/2;
	_MargeSort(a,tmp,begin,mid);
	_MargeSort(a,tmp,mid+1,end);
	int begin1=begin,end1=mid;
	int begin2=mid+1,end2=end;
	int index=begin;
	
	while(begin1<=end1&&begin2<=end2)
	{
		if(a[begin1]<a[begin2])
		{
			tmp[index++]=a[begin1++];
		}
		else
		{
			tmp[index++]=a[begin2++];
		}
		while(begin1<=end1)
		{
			tmp[index++]=a[begin1++];
		}
		while(begin2<=end2)
		{
			tmp[index++]=a[begin2++];
		}
		memcpy(a+begin,tmp+begin,(end-begin+1)*sizeof(int));
}

void  MargeSort(int*a,int n)
{
	int tmp=(int*)malloc(n*sizeof(int));
	if(tmp==NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	_MargeSort(a,tmp,0,n-1);
	free(tmp);
}

在这里插入图片描述
空间复杂度:O(N)
时间复杂度:O(N*logN)

2.6.3. 非递归代码实现

非递归的思路和上面的思路类似,一一归并,两两归并,四四归并。

  • 第一次对数组进行一一归并,每一个一组,每两组进行归并,当整个数组都归并完之后,归并后的tmp数组中,每两个数之间是有序的,此时将tmp覆盖在a数组
  • 第二次对数组进行二二归并,每两个一组,每两个进行归并,当整个数组都归并完之后,归并后的tmp数组中,每四个数之间是有序的,此时将tmp覆盖在a数组

  • 请添加图片描述
  • 当最后一次归并完后,tmp也就变成有序的一个数组,将其覆盖在a数字上即可
  • 下面的代码,gap是我们控制的每组数的数量,gap为一,一个数为一组,每两组进行归并
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//gap=1,i=0
//begin1=0,end1=0,begin2=1,end2=1,每组有一个数
//gap=2,i=0;
//begin1=0,end1=1,begin2=2,end2=3;每组有两个数
void MergeSortSNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n*sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, (2 * gap) * sizeof(int));
		}
		gap *= 2;
	}
	free(tmp);
} 

在这里插入图片描述
请添加图片描述
这里可以看见程序是成功运行了,但是真的没问题吗
我们试试10位数据的情况
在这里插入图片描述
10位数据的情况下,程序崩了,还出现了越界的情况请添加图片描述
我们可以看到,因为这个gap每次是2的倍数,循环的条件是i<n,也就是说当begin1<n的时候,此循环还会进行,但是只有begin1是小于1的,我们不能保证进入循环后的end1,begin2,end2都小于n,也就是说end1,begin2,end2存在越界的情况

  • 在加入输出每组begin1,end1,begin2,end2的区间后,可以发现,除了第一次循环,后面的循环均出现了越界的情况
    在这里插入图片描述
    在8位数(2的次方数)的情况下begin2刚好是数组最后一个元素,下一次的i==n,进不去循环,所以没发生越界
    在这里插入图片描述

2.6.4. 非递归修正

首先,我们上面的代码,begin1不可能发生越界,上一个end2是数组最后一个元素的话,begin1就不会进入下一次循环。
根据上面的结论,最可能出现越界的是end1,begin2,end2,我们要处理好这三个越界的情况

  • 首先,当begin2越界的时候,因为上一组我们已经将对应位置排好序,所以这块的数据可以不用排,直接跳过在这里插入图片描述
  • 并且,如果是end1越界,由于上组已经将序排好,这里也可以选择直接跳出这次循环在这里插入图片描述
  • 如果是end2越界,我们只需要将嗯end2的位置移到最后一个元素位置即可,因为我们的数组合并,是两个有序数组进行合并,对数组的长度没有要求,所以这里end2位置只要不小于begin2,不大于n即可。在这里插入图片描述
    end1越界,begin2一定越界,只要当begin2越界的时候,也就是右边没有可比较的数组时,就可以break;
    所以判断条件可以写成(begin2>=2)

		if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2>=n)
			{
				end2 = n - 1;
			}

虽然但是,他还是崩了
在这里插入图片描述
我们再看一下上面的代码

memcpy(a + i, tmp + i, (2 * gap) * sizeof(int));
//(2*gap)不是修正后的空间大小
//(end2-i+1)是空间大小,这里不能使用begin1,在上面的循环中,begin1的值发生了变化
//修改后:
memcpy(a+i,tmp+i,(end2-i+1)*sizeof(int));

修改后

void MergeSortSNonR(int* a, int n)
{
	int* tmp = (int*)calloc(n, sizeof(int));
	if (tmp == NULL)
	{
		perror("malloc falied");
		exit(-1);
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = i;
			if (begin2 >= n )
			{
				break;
			}
			if (end2>=n)
			{ 
				end2 = n - 1;
			}
			printf("[ %d , %d ], [ %d , %d ]", begin1, end1, begin2, end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
			memcpy(a + i, tmp + i, (end2-i+1) * sizeof(int));

		}

		gap *= 2;
		printf("\n");
	}
	free(tmp);
}

完美运行
在这里插入图片描述

2.7.计数排序(非比较排序)

2.7.1思路及实现

计数排序就是字面意思,不需要比较数组中元素的大小,只需要统计每个元素出现的次数
请添加图片描述

这种结构类似哈希表
也属于映射–每个值与位置建立一个关系,这里将数组中的值直接当做数组下标,采用的是绝对映射
当数据不是从0开始的
在这里插入图片描述

这里值是从100开始的,如果还使用绝对映射,前面99块空间都会被浪费,这里可以采用相对映射,从最小的100,到最大的199,开辟99块空间
在这里插入图片描述
中间元素的下标可以使用a[i]-min来表示,如果想要找回原来的值,只需要a[i]+min即可。
特点:

  1. 计数排序适合数据很集中的数组,如果一个数组中只含有100个同一个数字,那么只需要开辟一块空间即可,但是如果一个数组中只含有1和9999999,那么需要开辟9999999的空间
  2. 这是个小众排序,没办法排序浮点数,字符串,或者是结构体,但是对于数据集中的整形来说,有奇效,超过快排和希尔都很简单
    代码实现:
void CountSort(int* a, int n)
{
        int min = a[0], max = a[0];
        for (int i = 0; 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));
        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;
                }
        }
} 

2.7.2高端局

注:rand()只能产生最多3万个随机数,因为数据较为集中,所以这种对比情况下,计数排序很占优势
10w个数据(无序)
在这里插入图片描述
100w个数据
在这里插入图片描述
1000万数据
在这里插入图片描述
1亿数据
在这里插入图片描述
计数排序
时间复杂度:O(N+range)
空间复杂度:O(range)

3. 拓展

3.1.内外排序

  • 内排序:数据在内存中,量较少,可以直接通过开辟空间或者对数组进行修改排序
  • 外排序:数据在磁盘中,量很大,内存装不下,对这种数据的排序
    在这里插入图片描述
    归并排序是很好的能外排序的方法
    假设我们有10亿个数据(约4G),但是我们的内存只有1G
    我们可以采用归并的思想
  • 先将4G的文件平均,分为4个1G的文件,保证文件都可以放在内存中,然后采用归并的思想,先将4个1G的文件排序,然后一个文件为一组,用文件指针对2个文件同时进行归并,归并后的结果放在2G的文件中,之后在对2个2G的文件进行归并,结果放入4G的文件
  • 注:读取和存放我们可以每次只读取一个,存放一个,这样能保证内存够用
    在这里插入图片描述

3.2.稳定性

稳定性:相同数据排完序后,相同的数据相对位置是否变化
在这里插入图片描述
比如在上面的数组中,有三个5,黑5最前,红5中间,绿5最后,如果排完序后,三个5的位置不发生变化,可以说排序是稳定的,相反,如果排完序后,三个5的顺序发生变化,就是不稳定的排序
稳定性的意义:
比如,有个结构体,存储的信息有总成绩和各科成绩,此时想要按总成绩排序,相同成绩的同学按语文成绩大小排序,在语文成绩有序的情况下,排总成绩,不稳定的排序可能会在排总成绩的过程中,将相同总分下的语文成绩顺序打乱,稳定的排序会保证相同总成绩下的语文成绩依旧有序

3.3.排序的对比

在这里插入图片描述

1. 直接插入排序

  • 时间复杂度:
    最坏的情况下,也就是所有数据都是逆序,此时第几次循环就要交换几次位置,从一次到n-1次所以为O(N^2)
  • 最好的情况下,数据顺序,每次都不需要交换,此时为O(N)
  • 空间复杂度:
    不需要开辟额外空间,为O(1)
  • 稳定性:
    每次都会与前面大的数据交换,小于或等于不会交换位置,所以稳定
    2. 希尔排序
  • 时间复杂度:
    前面的结论:O(N^1.3),由大量试验得出的结论
  • 空间复杂度:
    不需要开辟空间,O(1)
  • 稳定性:
    由于在预排序过程中,将数组原数据的顺序已经打乱,比如数组中有两个9,第一个9预排序在第一组,第二个9在第二组,排完序后,第一个9很可能会被排到第二个9后面的位置,最后在稳定的插入排序下,9的位置会互换,所以不稳定
    3. 选择排序
  • 时间复杂度:
    数组从begin=0向右走,end=n向左走,begin和end相遇时停止,每次begin和end走一步,都要在中间的数据中找最大和最小,两层循环,外面走n/2,内层每次走(end-begin)次,时间复杂度为O(N^2)
  • 空间复杂度:
    不开辟额外空间,O(1)
  • 稳定性:
    在这里插入图片描述

从左往右看,每次找最小值,如果是上面的情况,因为是交换的原因,min找到的第一个3和1会进行一次交换,此时,两个3的顺序不同,所以不稳定
从右往左看,每次找最大值,如果有两个9,第一次max找到第一个9,第二个9不会替换max的值,最后前面的9会替换end,导致两个9的顺序改变,因此对max来说,不稳定
因此,得出结论,找大和找小均存在的情况下,不稳定
4.堆排序

  • 时间复杂度:
    O(N*logN)
  • 空间复杂度:
    O(1)
  • 稳定性:
    在这里插入图片描述
    堆顶的9和最下面的2进行位置互换,此时在物理结构的数组中,两个9的前后位置发生变化,所以不稳定
    5. 冒泡排序
  • 时间复杂度:
    未优化的情况下:因为每次只要前面大于后面都要进行交换,每次循环两个两个对比,所以不管有没有序,都是O(N^2)
    优化后的版本:在前面数据均有序的情况下能少点
  • 空间复杂度:
    O(1)
  • 稳定性:
    作为上面所有排序中最慢的,冒哥起码很稳定
    相等数据的情况下,后面不可能跑到前面,稳定
    6.快速排序
  • 时间复杂度
    类似二叉树的结构,每次将数组分成两段,两段子序列再分别分成两段,为logN,每次调整位置,需要走n/2次,所以最后的时间复杂度O(N*logN)(这里是优化后的),没有优化的情况下要分情况讨论
  • 空间复杂度
    因为程序采用了递归的方法,每次递归都会在栈上使用空间,使用的空间也就是类似二叉树的层数O(logN)
  • 稳定性
    每次是将标记的位置放在合适的位置,为了避免死循环,我们不得不在数据相同的时候也进行替换,因此不稳定
    7.归并排序
  • 时间复杂度:
    归并的思路,将所有数据一一归并,二二归并,四四归并等,每两组的数据进行排序,直到所有数据有序,很稳定的0(N*logN)
  • 空间复杂度:
    需要开辟n快空间保存原数组的数据,所以为O(N)
  • 稳定性:
  • 每次前后两个数组进行归并,相同数据也是左边数组的先排,所以稳定
    总结:

在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值