快排的PARTITION过程,荷兰国旗问题,leetcode324摆动排序问题分析比较总结

本文提供了一套固定的分析模式,帮助读者正确清晰地理解PARTITION过程、荷兰国旗问题及leetcode324摆动排序等算法,强调了算法分析的共性和区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上述三个问题具有一些共性,本文给出这种共性的描述与总结,然后给出一套固定的分析模式,指导读者如何***正确清晰***的去分析这类问题。

注:正确清晰指的是,按照本文给的这种模式去分析,写代码的时候会很少出错,思考的思路有非常有迹可循。
注2:理解本文的前提,需要读者对上述三个问题有一个比较具体的了解,对三个问题的O(n)时间复杂度的解法知道。

三个问题的共性与区别

注:此处特指O(n)复杂度的解法。

共性: 通过索引将数组划分成已处理部分与未处理部分。其中已处理部分指的是,已经符合某种条件了。
然后在一遍遍历的过程中,通过数组元素的交换操作,与索引的更新操作,不断的扩充已处理部分,缩小待处理部分,直至待处理部分大小为0,算法结束。此时整个数组处于一种符合我们要求的状态。

区别:

  1. 已处理部分与未处理部分的划分方式不同
  2. 已处理部分符合的条件不同
  3. 由于划分条件的不同,所以导致索引的初始化与遍历的终止条件不同
  4. 由于已处理部分符合的条件不同,所以在遍历过程中的交换操作与索引的更新操作是不同的。

分析的固定模式

在分析的过程中先思考下述问题:

  1. 已处理部分与未处理部分如何划分?由此导致的索引的初始值应该是什么?
    初始的时候,已处理部分长度肯定是0;未处理部分长度肯定是整个数组长度;
    在整个过程中已处理部分与未处理部分是数组的一个划分(即不交叉,相加后是整个数组)。

  2. 已处理部分应该符合什么条件?
    这个当然需要具体问题具体分析了。但是万变不离其宗:当已处理部分为整个数组的时候,应该使得整个数组处于一种我们需要的状态。(不一定就是最终的解)

  3. 遍历的终止条件是什么?
    显然,这也是具体问题具体分析了。但是万变不离其宗:终止条件就是未处理部分的长度为0

  4. 在遍历的过程中,如何根据遍历的元素值不同,怎么通过元素的交换操作,与索引的更新操作,确保已处理部分的扩充而且性质没有变化呢?
    这就完全是具体问题具体分析了。但是牢记这个过程的模式,都是根据遍历的元素不同,然后进行两部分操作:元素的交换索引的更新。达到一个目标:每次迭代的时候将已处理部分大小扩充1,且保持扩充后的已处理部分仍然符合2中的条件。

注:由于遍历过程中的每次迭代处理一个元素,已处理部分的扩充也应该是每次迭代扩充1。

小结:后续针对具体问题,我们就按照上述模式思考;然后根据具体问题分析,如何使用代码针对具体问题表达上述分析结果。

示例:PARTITION过程

注:此处描述的PARTITION过程,相较于快排中略有不同。

问题描述:给定一个数组A[l…r],一个pivot;将数组A原址重新排列,使得A的左半部分全部小于等于pivot,右半部分全部大于pivot。

问题分析:

  1. 已处理部分与未处理部分如何划分?索引的初始值是什么?
      根据题目描述,数组显然划成三部分:最左边的小于等于pivot的部分;中间的大于pivot的部分;右边的未处理部分。索引引入两个索引i,j就可以进行划分了:
      A[l,i] 是小于等于pivot的部分;A[i+1,j-1]是大于pivot的部分;A[j,r]是未处理部分;A[j]是当前迭代正在处理的元素。
      所以各索引的初始值为:
      i = l-1; j = l;

  2. 已处理部分符合什么条件?
      第一部分已经描述了,已处理部分应该有:
      A[l,i]的元素全部小于等于pivot;A[i+1,j-1]全部大于pivot。接下来我们检查就会发现,当未处理部分长度为0的时候,根据上述性质,显然整个数组被划分成左右两部分了:左边是小于等于pivot的;右边是大于pivot的。即符合我们的要求。

  3. 遍历的终止条件是什么?
      遍历的终止条件即未处理部分长度为0。在本题中即A[j,r]长度为0,所以即j=r+1。

  4. 遍历过程中的每次迭代,元素交换操作,索引更新操作?

    if (A[j] <= pivot)
    	i++;
    	swap(A[i], A[j]);
    	j++;
    else (A[j] > pivot)
    	j++;
    /*
    分情况分析:两种情况下都能够将已处理部分扩充1,且保持扩充后的已处理部分符合2中描述条件。
    详见:https://blog.youkuaiyun.com/LemintC/article/details/94549317
    */
    

    代码:

  9 void quickSortPartition(vector<int> &A, int pivot)
 10 {
 11 /*
 12         quicksort patition过程略有变化——————将数组A按照pivot划分成左右两部分
 13                 其中左边<=pivot,右边>pivot
 14 
 15         数组A[l...r]
 16 
 17         A[l,i]          已处理部分中<=pivot的
 18         A[i+1, j-1]     已处理部分中>pivot的
 19         A[j,r]          未处理部分
 20         A[j]            下一待处理元素
 21 */
 22 
 23         int l = 0;
 24         int r = A.size()-1;
 25         int i = l-1;
 26         int j = l;
 27 
 28         for(j = l; j <= r; j++) //遍历的终止条件
 29         {
 30                 if(A[j] <= pivot)
 31                 {
 32                         i++;
 33                         int temp = A[i];
 34                         A[i] = A[j];
 35                         A[j] = temp;
 36                         //j++
 37                 }
 38                 //else
 39                 //{
 40                         //j++
 41                 //}
 42         }
 43 }

示例:荷兰国旗问题

问题描述: 给定一个由0,1,2构成的数组,将数组原址重新排列,使得其左侧全部是0;中间全部是1,右侧全部是2。

问题分析:

  1. 已处理部分与未处理部分如何划分?索引的初始值是什么?
       将数组划分成四部分:左边全部为0;其次全部为1;右边全部为2;中间全部是未处理的,所以需要三个索引:i j k。
       A[l,i] 是全部为0的部分;A[i+1, j-1] 是全部为1的部分;A[k,r]是全部为2的部分;A[j,k-1] 是未处理部分;A[j]是当前迭代正在处理部分。
       所以各索引初始值为:
       i = l-1; j = l; k = r+1;

  2. 已处理部分符合什么条件?
       1中已经描述,接下来我们检查上述条件。当未处理部分长度为0的时候,显然数组被划分成了三部分:左边的全为0;中间的全为1;右边的全为2。符合我们的最后要求。

  3. 遍历的终止条件是什么?
       即未处理部分长度为0,即A[j,k] 长度为0,所以当k-1 < j的时候,停止迭代。

  4. 遍历过程中的每次迭代,元素交换操作,索引更新操作?

if( A[j] == 0)
	i++;
	swap(A[i], A[j]);
else if(A[j] == 1)
	j++;
else if(A[j] == 2)
	k--;
	swap(A[j],A[k]);
		//注意此处,没有j++;而是k--。然后将一个未处理的元素交换到了j的位置,所以j不需要j++;
		因为j还是指向的未处理元素。
	/*
	前两种情况与PARTITON过程一样。值得注意的是,由于交换操作,将一个未处理的元素交换到了j的位置,
	所以j还是指向的未处理元素,所以不需要通过j++更新。
	*/

代码:

 22 void hollandFlag(vector<int> &A)
 23 {
 24         int l = 0;
 25         int r = A.size()-1;
 26         int i = l-1;
 27         int j = l;
 28         int k = r+1;
 29 
 30         while(j <= k-1)
 31         {
 32                 if(A[j] == 0)
 33                 {
 34                         i++;
 35                         int temp = A[i];
 36                         A[i] = A[j];
 37                         A[j] = temp;
 38                         j++;
 39                 }
 40                 else if(A[j] == 1)
 41                 {
 42                         j++;
 43                 }
 44                 else    //A[j] == 2
 45                 {
 46                         k--;
 47                         int temp = A[k];
 48                         A[k] = A[j];
 49                         A[j]=temp;
 50                         //注意,此处不需要j++的,因为k--后将一个未处理部分,交换到j的位置了。
 51                 }
 52 
 53         }
 54 
 55 }

示例:leetcode324摆动排序

问题描述:

问题分析:

  5         1、如何划分已处理部分与未处理部分(以及划分的初始情况)?
  6                 A[0...n]        是完整数组
  7 
  8                 A[0...odd]的奇数部分
  9                 A[even, n]的偶数部分    为已处理部分
 10                 A[0,j-1]范围内的所有    也是已处理部分
 11 
 12                 剩下部分为未处理部分
 13 
 14                 A[j]    是下一待处理元素
 15 
 16                 初始化: odd = -1
 17                         even = n+1(r为奇数) 或者 n+2(r为偶数)
 18                         j = 0
 19 
 20                 注:显然,从上面描述可以知道,如何高效的判断一个元素是否被处理比较麻烦的。
 21                         A[i] 若有 (i <= odd && i%2 == 1) || (i >= even && i%2 == 0) || (i < j)
 22                                 表示A[i]已经得到处理
 23 
 24 
 25         2、已处理部分符合何种要求(核心是,已处理部分为整个数组的时候,满足题目要求)?
 26                 A[l,odd]的奇数部分都是大于中位数的
 27                 A[even, r]的偶数部分都是小于中位数的
 28 
 29                 最后将中位数填入剩下部分——————能够确保满足条件么?请证明
 30 
 31         3、遍历的终止条件?(核心是,终止的时候,所有元素都得到处理)
 32                 j > n
 33 
 34         4、如何在遍历的过程中通过元素交换,索引更新,扩充已处理部分且保持性质不变。(两大操作:元素交换,索引更新)?
 35                 -访问到的元素小于中位数
 36                         if(A[even-2] 已处理) flag=1;
 37                         else flag = 0;
 38                         even-=2
 39                         swap(even, j)
 40 
 41                         flag==1 ? j++ : j=j;
 42 
 43                 -访问的元素等于中位数
 44                         j++
 45 
 46                 -访问的元素大于中位数
 47                         if(A[odd+2]) 已处理 flag = 1;
 48                         else flag = 0
 49                         odd+=2
 50                         swap(odd, j)
 51                         flag == 1 ? j++ : j=j;
 52 
 53 
 54 注:上述索引的下标都是从0开始的。
 55 注2:根据朴素算法2中的正确性分析,我们可以不需要排序,只需要使用quickSort中的PATITION将数组划分成大于中位数与小于中位数的部分,然后按照上述规律
 56         填入一个新的数组,这样时间复杂度也是O(n),但是不是原址处理,所以空间复杂度也是O(n)。

代码:

 61         wiggleSort(A[0...n])
 62                 pivot = getPivot(A); //可以在O(n)时间内实现
 63                 int odd = -1;
 64                 int even = (n%2 == 0) ? n+2 : n+1;
 65                 int j = 0;
 66 
 67                 while(j <= n)
 68                 {
 69                         if(A[j] < pivot)
 70                                 flag = getPrcessed(A[even-2]);
 71                                 even -= 2;
 72                                 swap(A[even], A[j]);
 73                                 j = (flag == 1) ? j++ : j;
 74                         else if(A[j] == pivot)
 75                                 j++;
 76                         else //A[j] > pivot
 77                                 flag = getProcessed(A[odd+2]);
 78                                 odd += 2;
 79                                 swap(A[odd], A[j]);
 80                                 j = (flag == 1) ? j++ : j;
 81                 }
86 注:该问题最大的不同就是划分是分离且离散的。
 87         在扩充已处理部分的其中一个子部分的时候,不会影响其他子部分,索引odd与even之间的索引更新不会互相影响。(前面两题则不同)
 88 
 89         另外用于遍历的索引,j的更新则完全依赖于交换给j位置的元素有没有得到处理,交换过来的是已经处理的则j++;若没有处理,则j不变。
 90                 在前面两个问题中,由于划分是连续的,所以交换过来的元素可以很容易知道是否已经被处理了。而此处因为离散的关系,所以
 91                 增加一个getProcessed过程,判断要被交换的元素是否得到处理了。

注意:一些非常细微的注意的地方

1、索引的初始值一定不能够相同。具体原因见https://blog.youkuaiyun.com/LemintC/article/details/94549317开头描述。

参考:

https://blog.youkuaiyun.com/LemintC/article/details/94549317
https://blog.youkuaiyun.com/LemintC/article/details/98748471

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值