算法导论 — 思考题7-4 快速排序的栈深度

快速排序的栈深度)7.1节中的QUICKSORT算法包含了两个对其自身的递归调用。在调用PARTITION后,QUICKSORT分别递归调用了左边的子数组和右边的子数组。QUICKSORT中的第二个递归调用并不是必须的。我们可以用一个循环控制结构来代替它。这一技术称为尾递归,好的编译器都提供这一功能。考虑下面这个版本的快速排序,它摸拟了尾递归的情况:
  在这里插入图片描述
  a. 证明: T A I L R E C U R S I V E Q U I C K S O R T ( A , 1 , A . l e n g t h ) {\rm TAIL-RECURSIVE-QUICKSORT}(A, 1, A.length) 能正确地对数组 A A 进行排序。
  编译器通常使用来存储递归执行过程中的相关信息,包括每一次递归调用的参数等。最新调用的信息存在栈的顶部,而第一次调用的信息存在栈的底部。当一个过程被调用时,其相关信息被压入栈中;当它结束时,其信息则被弹出。因为我们假设数组参数是用指针来指示的,所以每次过程调用只需要 O ( 1 ) O(1) 的栈空间。栈深度是在一次计算中会用到的栈空间的最大值。
  b. 请描述一种场景,使得针对一个包含 n n 个元素数组的TAIL-RECURSIVE-QUICKSORT的栈深度是 Θ ( n ) Θ(n)
  c. 修改TAIL-RECURSIVE-QUICKSORT的代码,使其最坏情况下栈深度是 Θ ( l g n ) Θ(lgn) ,并且能够保持 O ( n l g n ) O(n{\rm lg}n) 的期望时间复杂度。
  
  
  a.
  TAIL-RECURSIVE-QUICKSORT所做的事情与QUICKSORT完全一样。
  在 Q U I C K S O R T ( A , p , r ) {\rm QUICKSORT}(A, p, r) 函数中,在递归调用 Q U I C K S O R T ( A , p , q 1 ) {\rm QUICKSORT}(A, p, q-1) 后,紧接着递归调用 Q U I C K S O R T ( A , q + 1 , r ) {\rm QUICKSORT}(A, q+1, r) 。即在确定划分子数组后,选递归排序左边的子数组,再递归排序右边的子数组。
  而在 T A I L R E C U R S I V E Q U I C K S O R T ( A , p , r ) {\rm TAIL-RECURSIVE-QUICKSORT}(A, p, r) 函数中,在递归调用 T A I L R E C U R S I V E Q U I C K S O R T ( A , p , q 1 ) {\rm TAIL-RECURSIVE -QUICKSORT}(A, p, q-1) ,即排序左边的子数组之后,紧接着让 p = q + 1 p = q+1 ,再进入下一轮迭代。在下一轮迭代中,再排序右边的子数组。
  
  b.
  每次递归调用TAIL-RECURSIVE-QUICKSORT都是针对左边的子数组。如果一个数组已经按单调递增顺序排好序,对该数组调用PARTITION后,所得到的 2 2 个子数组中,左边的子数组规模为 n 1 n-1 ,右边的子数组规模为 0 0 。左边规模为 n 1 n-1 的子数组通过递归调用TAIL-RECURSIVE-QUICKSORT来排序。因此,可以得到规模为n的数组的递归栈深度为 D ( n ) = D ( n 1 ) + 1 D(n) = D(n-1) + 1 ,这个递归式的解为 Θ ( n ) Θ(n)
  
  c.
  可以考虑这样改进:每次递归调用TAIL-RECURSIVE-QUICKSORT都排序规模较小的子数组。
  在这里插入图片描述
  这个算法的最坏情况发生在每次调用PARTITION都产生一个均衡的划分。这种情况下,栈的深度可以表示为 D ( n ) = D ( n / 2 ) + 1 D(n) = D(n/2) + 1 ,其解为 Θ ( l g n ) Θ({\rm lg}n)
  由于这个算法实质上与QUICKSORT一致,所以它的期望时间复杂度依然为 Θ ( n l g n ) Θ(n{\rm lg}n)
  
  代码链接:https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter07/Problem_7-4/TailRecursiveQuicksort

猜你喜欢

转载自blog.csdn.net/yangtzhou/article/details/88776394