STL中的list::sort算法解析

References

  1. 《STL源码剖析》
  2. http://www.ecjtu.org/thread-30140-1-1.html
  3. 《算法导论》
  4. http://blog.yangzhe1991.org/2011/01/stl-sort%E6%BA%90%E7%A0%81%E5%89%96%E6%9E%90/ : <algorithm> sort解析,使用的是RandomAccessIterator。

Overview

在侯捷的书中,说到了list::sort的实现是用quick sort。查找了一下网上,也发现有人说是快排,也有人说是归并排。但是实际上,我分析了一下,使用的应该是merge sort的非递归实现。
首先我们来看一下源码,如下:
// list 不能使用STL 算法 sort(),必须使用自己的 sort() member function,
// 因为STL算法sort() 只接受RamdonAccessIterator.
// 本函式采用 quick sort.
template <class T, class Alloc>
void list<T, Alloc>::sort() {
// 以下判断,如果是空白串行,或仅有一个元素,就不做任何动作。
if (node->next == node || link_type(node->next)->next == node) return;

// 一些新的 lists,做为中介数据存放区
list<T, Alloc> carry;
list<T, Alloc> counter[64];
int fill = 0;
while (!empty()) {
    carry.splice(carry.begin(), *this, begin());
    int i = 0;
    while(i < fill && !counter[i].empty()) {
      counter[i].merge(carry);
      carry.swap(counter[i++]);
    }
    carry.swap(counter[i]);        
    if (i == fill) ++fill;
}

for (int i = 1; i < fill; ++i)
     counter[i].merge(counter[i-1]);
swap(counter[fill-1]);
}

Essence of Merge Sort and Quick Sort

我们回忆一下merge sort的递归算法实现,大致如下:
void mergesort(int array[], int p, int q)
{
  if (p < q)
  {
    int n = (p + q) / 2;
    mergesort(array, p, n);
    mergesort(array, n + 1, q);
    merge(array, p, n, q);
  }
}
其中比较重要的就是我们假设merge sort完了之后,数组是已经排序好的了。 并且在divide的时候,数组是被等分成两部分的。最后是做一个merge。有一点要注意,merge是out of place的.

而快排的递归实现如下:
void quicksort(int array[], int p, int r)
{
  if (p < r)
  {
    int q = partition(array, p, r);
    quicksort(array, p, q - 1);
    quicksort(array, q + 1, r);
  }
}
其中最重要的一步就是partition,这个partition是在数组里面找到array中选定的一个元素在数组array[a:b]中的分界点,也就是说在array[a:n - 1]的所有元素都比array[n]小,array[n + 1:b]都比array[n]大。而重要的partition是in place的.

Analysis

OK,有了上面的思路,我们可以看下到底list::sort是怎么实现排序的。
// 以下判断,如果是空白串行,或仅有一个元素,就不做任何动作。
if (node->next == node || link_type(node->next)->next == node) return;
这部分是终止条件,很简单,可以跳过。

// 一些新的 lists,做为中介数据存放区
list<T, Alloc> carry;    // 1
list<T, Alloc> counter[64];  // 1
int fill = 0;
while (!empty()) {  // 2
    carry.splice(carry.begin(), *this, begin());  // 3
    int i = 0;
    while(i < fill && !counter[i].empty()) {   // 4 重点
      counter[i].merge(carry);
      carry.swap(counter[i++]);
    }
    carry.swap(counter[i]);  // 5
    if (i == fill) ++fill;  // 6
}
  1. 数据结构。注意到这里counter是一个有64个元素的数组。这两个变量的用法在后面的解析中会讲述。
  2. while循环,在本身这个list为空的情况下会结束。
  3. 由于carry是空的(第一次循环的时候是空的,至于后面的循环是否为空后面可以检查一下),这一行的用法实际上就是把list的第一个元素给carry。此时carry应该只有一个元素。
  4. 注意到fill一开始是为0的,随着循环的增加而增加,而i在每次循环的开始都为0. 注意到里面的这个while循环在是在i < fill并且counter数组的第i个元素不为空的情况下才进行。那么这个循环做了什么呢?
    1. 首先,它将carry和counter[i]进行merge。注意merge完之后,carry为空。
    2. 然后将counter[i]的结果和carry进行swap,实际上也就是把counter[i]的内容移到了carry。此时i自增1. 注意此时swap非空。
  5. 最后当内循环结束之后,carry和counter[i]进行swap。4中的while的条件我们可以知道,在4结束后,要不就是i >= fill,要不就是counter[i]为空。暂且我们认为是第二个条件成立,那么此时就是carry非空,而counter[i]为空。所以结果就是carry为空,counter[i]不为空。
  6. 这个if的条件成立的时候,实际上也就是4在第一个条件不成立的时候结束的。此时fill加1.
由上面的分析我们可以知道,在每次循环的开始,carry都是为空。所以carry相当于就是一个临时变量的感觉,它帮我们carry了list中的第一个元素到counter中的某一个元素中的list中了。
那么到底counter是怎么用的呢?
我们来看一个例子,假设list最开始是[2, 4, 3, 1]。程序的几次循环就会如下面所说的那样运行(显示的变量值为每次循环的最后结果):
  1. this: [4, 3, 1]
    carry: []
    i: 0
    counter[0]: [2]
    fill: 1
  2. this: [3, 1]
    carry: []
    i: 1
    counter[0]: []
    counter[1]: [2, 4]
    fill: 2
  3. this: [1]
    carry: []
    i: 0
    counter[0]: [3]
    counter[1]: [2, 4]
    fill: 2
  4. this: []
    carry: []
    i: 2
    counter[0]: []
    counter[1]: []
    counter[2]: [1, 2, 3, 4]
    fill: 3
我想大家运行完上面的程序,大概对于整个循环的结构都多少心中有数。就是fill实际指的是当前counter中,用到了的元素的个数。而counter中,第i个元素的会有0或者2^(i+1)个元素。
而每次counter相邻两个元素的进行merge(比如说i, i+1),就可以生成(merge)下一个元素所能容纳的元素个数。
回想一个mergesort的实质,如果我们反过来思考,从最基础的元素开始,一个一个的merge,直到最后的数组,那么就可以得到类似的结论。
就是说,而这种自底向上的方法,就是一般递归算法的迭代解的思路。
所以说,list::sort实际上是一个mergesort的迭代解法,不过因为是迭代解,没有办法做到in place,只能利用两个辅组变量来达成排序。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值