【算法总结】快速排序及边界问题分析

1. 快速排序——分治

# 算法原理:

【算法总结】快速排序及边界问题分析_第1张图片

在给定序列找到一个点x使得x左边区间数都小于x,右边区间数都大于x

# 步骤:

  1. 确定分界点
    • 随机,可以是第一个数
  2. 调整区间
    • 使左边都小于分界点,右边都大于分界点
  3. 递归处理左右两段
    • 递归停止的条件if(l >= r) return;即区间里没有数或只有1个数就直接返回

# 如何实现x左边都小于等于x右边都大于等于x?

方法一:暴力法
  1. 开辟两个数组 a[]b[]
  2. 对序列q[l~r]:小于x的放a[] 大于x放b[]
    • if q[i] <= x then q[i]-->a[]
    • if q[i] > x then q[i] -->b[]
  3. a[],b[]依次放回q[]
    • a[],b[]-->q[]
方法二:双指针

【算法总结】快速排序及边界问题分析_第2张图片

  1. i指针指向的数小于x,指针后移
    • q[i]i++
  2. 直到i指向的数大于x,开始判断j指向的数
    • q[i]>x, 判断q[j]
  3. j指针指向的数大于x,指针前移
    • q[j]>xj--
  4. 直到j指向的数小于x,交换i,j指向的数
    • q[j]swap(q[i], q[j])
  5. 重复上述过程,直到i>j

最终j指针一定在i指针前面,i指向x

因为循环结束的条件是i,然后每次循环至少执行1次i++j--,因此在退出循环时,必定满足j + 1 = i,即ji的前面。后文有解析这个问题,点击跳转

  • i 左边的数永远 <=x
  • j 右边的数永远 >=x

i, j 两个指针穿过后,左边的数为<=x,右边的数位>=x,从而分成2个区间。

# 快排模板

void quick_sort(int q[], int l, int r)
{
	if(l >= r) return;
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while(i < j)
    {
        do i++; while(q[i] < x);
        do j--; while(q[j] > x);
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

# 需要斟酌的细节(边界问题)

参考了CSDN上总结的一篇非常Nice的文章

细节一:指针移动的判断不带等号

考虑一个边界问题,为什么移动ij指针的条件是q[i] < xq[j] > x,而不是q[i] <= xq[j] >= x

原因如下

  • 如果选取的x是数组里最大的数,序列中所有的数都满足q[i] <= x,会导致i会一直++发生越界都不会停下来。
  • 如果选取的x是数组里最小的数,同理q[j] >= x恒成立,j会一直--发生越界。

这也是造成快排不稳定的原因,排序算法是否稳定,与时间效率是否稳定无关。稳定是指若源序列中两个值相同的数,排序后这两个数的先后次序不会发生改变。

而快排中当边界点存在重复的数会交换位置。因此快排不稳定。

解决不稳定的方法:把序列中的数改成二元数,Ai改成 ,从而使所有的数都不相同。

细节二:使用do-while在判断前先移动指针

考虑一个边界问题,为什么不能让i = lj = r然后使用while循环代替do-while循环?

探讨一下写成如下形式会有什么问题

while(q[i] < x) i++;
while(q[j] > x) j--;

若数组中存在重复的数字,某一轮可能存在**ij都指向重复的数字**,并且分界点x也是这个数字,上述两个while语句的判断就会结束循环,此时q[i] = q[j] = x,交换ij指向这个局面仍然不会改变,因此下一轮会重复这个过程,陷入死循环。

因此,要确保每轮下来两个指针都至少会移动一步,保证上一次交换的结果不会再次判断。

思路是,在判断while条件时,先移动指针。用do-while最容易实现这个思想,也可以用while实现:

do i++; while(q[i] < x);
do j--; while(q[j] > x);
//等价于
while(q[++i] < x);
while(q[--j] > x);

同时为了保证每轮下来边界lr都能被判断到,因此初始化要i = l - 1, j = r + 1使指针在数组两端之外。

细节三:区间左半边使[l, j]而不是[l, i]

考虑一个边界问题,q[i]q[j]i == j - 1 时停下来做交换的场景,交换完成之后ij会各自前进(i ++, j --)一步,形成i > j(即i == j + 1)的不合法局面。

这个局面的原因:由于i指针移动时,其走过的数都满足< xj移动时其走过的数都满足> xi, j相遇时,其再继续移动就会穿过对方,进入对方走过的数,因此会停下来等待交换。此时i, j位置就发生了交换, j + 1 = i。并且i, j指向的数不用再交换了,因为前后的数都已经判断过了。

此时,从lj的数都满足q[l, j] <= x。为什么不是< x,因为i走过的数如果需要和j交换,换过来的数可能是= x的。

所以,在这个局面下,**满足性质<= x的区间是[l, j]**而不是[l, i],因此划分的两个区间是[l, j][j + 1, r]

细节四:关于x的取的位置

考虑一个边界问题,xq[l], q[r], q[l+r>>1], q[l+r+1>>1]会有什么不同的结果?区别在于上取整还是下取整。

当按照q[l+r+1>>1],递归区间[l, j], [j+1, r]时,会有以下边界问题:

【算法总结】快速排序及边界问题分析_第3张图片

最终i = j = 2,跟前面分析的不一样,最后结果也是进入死循环无限递归。

这个细节等二分的细节研究完再补充。

2022.04.10

你可能感兴趣的:(算法,c++,c语言,算法)