数据结构初阶之排序(二)

忙了一个月考试和课设,今天写点排序

今天主要内容是在排序中比较难的快排

一、快速排序

首先来说快速排序的定义:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。

快速排序是冒泡排序的改进。

快速排序一般有以下几种方法:

hoare法:

我们有一组待排序的数据,在其中选一个关键字key出来,一般是选数据的头数字或者尾数字

举个例子,用以下数据来进行hoare法排序

6 1 2 7 9 3 4 5 10 8

首先选key为6,设置begin和end两个指针,分别指向数据列的头和尾,接着end从右边出发,找比key小的数字,找到5,然后begin开始从左走,找比6大的数字,一直走到7,然后5和7互换,然后end继续从7向左出发继续找小,找到4,begin再出发,找到9,互换,end再出发找到3,begin再出发,此时就会和end相遇,交换key和begin end相遇的值,这样,单趟快速排序就完成了,接下来,key的左边的值比key小,key右边值比它大,再想办法让key左边区间有序,key的右边区间有序,整体就有序了。

其实上面这段话描述完,应当有两个疑问,当key为头的时候,为什么一定要右边先出发呢?为什么begin和end相遇的那个数字一定比key小呢?其实这是一个问题,因为begin和end相遇只有两种情况,一种是begin碰到end。另一种是end碰到begin

begin碰到end:end先走,end停在了比key小的位置上,如果此时begin后面的都比key小,那么begin就来到了end的位置停止了,而end就在比key小的位置上。

end碰到begin:end先走,end停在了比key小的位置上,begin再走,begin停在了比key大的位置上,将begin和end处的值交换,end再走,假设end前面的都比key小,那么end来到了begin的位置就停止了,此时相遇,此时相遇点处的值比key小了,为什么呢?因为前面begin处的值与end处的值发生了交换,而end处的值是小于key的。

接下来,我们就该考虑,如何让左右区间分别有序呢?

那就是递归

代码图如下

//单趟排序
int PartSort(int *a,int begin,int end)

{
    int keyi = begin;//选定一个关键字的下标
    while (begin < end)
    {
        //右边先走 右边先找比keyi小的
        while (begin < end && a[end] >= a[keyi])//这里要注意=的添加,不然就错了
        {
            end--;
        }
        //此时end是比keyi小的那个数的下标

        //左边找比keyi大的
        while (begin < end && a[begin] <= a[keyi])
        {
            begin++;
        }
        //此时begin是比keyi大的那个数的下标
        Swap(&a[begin], &a[end]);//交换
        //这里的目的是让左边是比关键字小的,右边是比关键字大的
    }
    //此时begin和end相遇了
    Swap(&a[begin], &a[keyi]);//交换相遇的地方和关键字的位置
    return begin;//返回关键字的位置

}
//递归
void QuickSort(int* a, int begin, int end)
{
    if (begin >= end)//如果区间不存在
    {
        return;
    }
    int keyi = PartSort(a, begin, end);
    QuickSort(a, begin, keyi - 1);
    QuickSort(a, keyi + 1, end);
}

快排时间复杂度

这个需要分类讨论:

1.理想情况下,单趟排序后key已经出于中间,那么递归深度为层数次,也就是logN,而每一层的时间复杂度都是N,所以总体复杂度就是相乘

image-20210917104247330

2.但大部分情况下都是不理想的,比如取个不好点的情况,数据有序,不论升序降序,如果想重新排一遍,那么一定每次有一边的区间都是空的,那么递归深度就到了N次,而每一层都需要N次,所以时间复杂度就square了。

所以这里可以提出一个优化的方案

随机选key或者是三数取中,实际中三数取中应用比较广泛。

下面实现三数取中

int GetMidIndex(int* a, int begin, int end)
{
    int mid = (begin + end) / 2;
    if (a[begin] < a[mid])
    {
        if (a[mid] < a[end])
        {
            return mid;
        }
        else if (a[begin] < a[end])
        {
            return end;
        }
        else
        {
            return begin;
        }
    }
    else
    {
        if (a[mid] > a[end])
        {
            return mid;
        }
        else if (a[begin] < a[end])
        {
            return begin;
        }
        else
        {
            return end;
        }
    }
}

三数取中之后就保证了不会出现上面说的只有一边有数的情况,所以基本就是最优了。

接下来介绍快排的另一种方法,思想都是一样的,方法大同小异

挖坑法

将最左边(右边也可以)的值用key变量保存起来,此时最左边(右边)形成一个坑位,然后end开始走(如果最右边找的key,那么begin先走),end找比关键字小的,找到后将坑位填充,然后坑位变为end,然后begin开始走,begin找比关键字大的,找到后将坑位填充,然后坑位变为begin,直到begin和end相遇,此时begin和end都是坑位,将之前保存的关键字的值填到坑里面

这种方法和hoare法其实差不多

int PartSort2(int* a, int begin, int end)
{
    int hole = begin;
   	int key=a[begin];
    while(begin<end)
    {
        //让右边先走找比key小的值,填到坑里
        while(begin<end && a[end]>=key)
        {
            end--;
        }
        //填到坑
        a[hole]=a[end];
        //更新坑
        hole=end;
        //左边走,找比key大的值,填到坑
        while(begin<end && a[begin]<=key)
        {
            begin++;
        }
        //填到坑
        a[hole]=a[left];
        //更新坑
        hole=begin;
    }
    //相遇
    a[hole]=key;
    return hole;
}

最后介绍一种快排法,

前后指针法

这种方法可以避开比较多的麻烦

前后指针法核心思想:

1.cur往前走,找比key小的数据

2.找到比key小的数据以后,停下来,prev++

3.交换prev和cur指向位置的值

4.直到cur走到数组的结尾,将key与prev交换   

这里我们选取第一个数为key,具体过程看下面动图

前后指针快速排序

cur还没遇到比key大的值前,prev紧跟着cur,cur遇到比key大的值后,prev和cur之间间隔了一段比key大的数据,所以当cur找到小于关键字的值时,prev++,将cur和prev交换,实际上就是将小于关键字的值换到左边,大于关键字的值换到了右边 

具体代码如下:

int PartSort3(int* a, int begin, int end)
{
	int prev = begin;
	int cur = begin + 1;
	int keyi = begin;

	// 加入三数取中的优化
	int midi = GetMidIndex(a, begin, end);
	Swap(&a[keyi], &a[midi]);

	while (cur <= end)
	{ 
		// cur位置的之小于keyi位置值
		if (a[cur] < a[keyi] && ++prev != cur)
			Swap(&a[prev], &a[cur]);

		++cur;
	}

	Swap(&a[prev], &a[keyi]);
	keyi = prev;

	return keyi;
}

以上三种就是常见的快排方法,但都存在一个致命的缺点,就是递归,一旦递归,就涉及到性能和栈溢出的问题,所以下面再写以下快速排序的非递归思想

 非递归思路:

这里我们借用数据结构的栈来实现

依次把需要单趟排的区间入栈,依次取栈里面的区间出来单趟排,再把需要处理的子区间入栈

还是用上面的数据举例子

6 1 2 7 9 3 4 5 10 8

此时begin是0,end是9,首先将9和0入栈,当栈不为空的时候,令left等于0,令right等于9,分别出栈,然后用设定好的left和right对a进行一次快速排序,排完之后就是3 1 2 5 4 6 9 7 10 8,此时begin也就是keyi指向6,值为5,而end指向3,值为0,接下里就判断左右子区间的存在性,然后把子区间入栈,再快排,最后直到入完栈。

void QuickSortNonR(int *a,int n)
{
    ST st;
    StackInit(&st);
    StackPush(&st,end);
    StackPush(&st,begin);
    while(!StackEmpty(&st))
    {
        int left = StackTop(&st);	
        StackPop(&st);
        
        int right = StackTop(&st);
        StackPop(&st);
        
        int keyi = PartSort1(a,left,right);
        //[left,keyi-1] keyi [keyi+1,right]
        if(keyi+1<right)//如果存在右序列(右序列需要大于1)
        {
            StackPush(&st,right);
            StackPush(&st,keyi+1);
        }
        if(keyi-1>left)//如果存在左序列(左序列需要大于1)
        {
            StackPush(&st,keyi-1);
            StackPush(&st,left);
        }
    }
    StackDestory(&st);
}

在快速排序中, 这个非递归其实就是目标堆栈是一个不断 入栈-出栈 的过程,在出栈的过程中,就对数据进行处理,没有必要再最后一次性处理。将递归的方式变为出入栈的方式,本质是一样的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

何以过春秋

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

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

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

打赏作者

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

抵扣说明:

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

余额充值