假设有一个容器,保存了 100 万个数值,但我们只对其中最小的 100 个元素的顺序感兴趣。可以对容器的全部内容排序,然后选择前 100 个元素,但这样处理会使效率低。这时候需要使用部分排序 partial_sort,高效地获得这些数中的前100个最小数的顺序。

1. partial_sort 接口说明

partial_sort有两个版本,一个默认以小于(less-than操作符)作为比较规则,出来的顺序为递增排列。另一个可以传入一个比较函数,即调用者指定一个比较规则。

partial_sort(beg,mid,end)
partial_sort(beg,mid,end,comp)

对容器中的(mid-beg)个元素进行排序,partial_sort 完成之后,从beg到mid(但不包括mid)范围内的元素时有序的,且大于(或者指定的comp函数)mid之后的元素。未排序元素之间的次序是不确定的,即partial_port不是稳定排序算法。

2. partial_sort 用法举例

下面这段代码演示了 partial_sort() 算法的用法:

size_t count {5}; // Number of elements to be sorted
std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};
std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));

执行partial__sort()后的效果如下图所示:

partial_sort() 算法的操作

需要注意的是,不能保持未排序元素的原始顺序。在执行 partial_sort() 后后面元素的顺序是不确定的,这取决于具体的实现。

也可以指定不同的比较函数。例如:

std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std:: end (numbers) , std::greater<>());

现在,number 中最大的 count 个元素会是容器开始处的一个降序序列。以上语句的输出结果为:

93 88 56 45 22 7 19 12 8 7 15 10

同样的,不能保持 numbers 中未排序元素的原始顺序。

3. partial_sort 原理概述

那么 partial_sort 的原理是什么呢?是堆排序!

partial_sort的原理:

  1. 对原始容器内区间为[first, middle)的元素执行 make_heap() 操作构造一个大顶堆,
  2. 然后遍历剩余区间[middle, last)中的元素,剩余区间的每个元素均与大顶堆的堆顶元素进行比较(大顶堆的堆顶元素为最大元素,该元素为第一个元素,很容易获得),若堆顶元素较小,则交换堆顶元素和遍历得到的元素值(pop_heap ),并重新调整该大顶堆以维持该堆为大顶堆(adjust_heap)。
  3. 遍历结束后,[first, middle)区间内的元素便是排名在前的m个元素,再对该堆做一次堆排序 sort_heap() 便可得到最后的结果。

下图为partial_sort 算法的步骤详解:

partial_sort() 算法步骤详解

4. partial_sort 源码讲解

下面是STL中 partial_sort 的源码讲解,考虑到篇幅,这里只列出第一个版本的。

// 版本一
template <class RandomAccessIterator>
inline void partial_sort(RandomAccessIterator first,
	RandomAccessIterator middle,
	RandomAccessIterator last) {
		__partial_sort(first, middle, last, value_type(first));
}
 
template <class RandomAccessIterator, class T>
void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
	RandomAccessIterator last, T*) {
		make_heap(first, middle); //将区间[first, middle)构造为一个堆结构
		for (RandomAccessIterator i = middle; i < last; ++i)
			if (*i < *first)    // 遍历堆以外的元素,并将更优的元素放入堆中
				__pop_heap(first, middle, i, T(*i), distance_type(first)); // first值放i中,i的原值融入heap并调整
		sort_heap(first, middle); // 对最终的堆进行排序
}

先来分析 make_heap 算法, 该算法将一段指定的数据排列出max-heap

template <class RandomAccessIterator>
inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) {
	__make_heap(first, last, value_type(first), distance_type(first));
}
 
template <class RandomAccessIterator, class T, class Distance>
void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,
	Distance*) {
		if (last - first < 2) return; // 如果长度为0或1,不必重新排列	
		Distance len = last - first;
		// 找出第一个需要重新排列的子树头部(即最后一个子树),以parent标记。由于任何叶子节点都不需要处理。
		// parent 命名不佳, 为 holeIndex 更好
		Distance parent = (len - 2)/2; 
 
		while (true) {
		    // 重排以parent为首的子树,以len为操作范围
			__adjust_heap(first, parent, len, T(*(first + parent)));
			if (parent == 0) return; // 走完根节点,结束
			parent--; // 向前排列前一个节点
		}
}

下面来重点分析 __adjust_heap 算法, 该算法调整从first开始的len个元素,洞号为holeIndex,洞值为value, 最终获得一个 max-heap

template <class RandomAccessIterator, class Distance, class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,
	Distance len, T value) {
		Distance topIndex = holeIndex;
		Distance secondChild = 2 * holeIndex + 2; // holeIndex的右子节点
		while (secondChild < len) {
			// 比较holeIndex两个子节点的值,用secondChild代表值较大的子节点
			if (*(first + secondChild) < *(first + (secondChild - 1)))
				secondChild--;   
			// 令较大子值为洞值,注意:原洞值已在函数形参value中得以保存
			*(first + holeIndex) = *(first + secondChild); 
			// 再让洞号下移到左子节点处,
			holeIndex = secondChild;
			// 算出新的洞节点的右子节点
			secondChild = 2 * (secondChild + 1);
		}
		// 如果没有右子节点,只有左子节点
		if (secondChild == len) { 
			// 令左子节点为洞值,然后将洞号下移到左子节点
			*(first + holeIndex) = *(first + (secondChild - 1));
			holeIndex = secondChild - 1;
		}
		// 将原洞值push到新的洞号中。
		// 以下语句的效果类似于: *(first + holeIndex) = value; 
		__push_heap(first, holeIndex, topIndex, value);
}

进一步讲解__adjust_heap 算法 调用的 __push_heap 算法,该算实现将新元素value push到max-heap中topIndex到holeIndex的合适位置中,其中max-heap的起始位置是first。

template <class RandomAccessIterator, class Distance, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex,
	Distance topIndex, T value) {
		Distance parent = (holeIndex - 1) / 2;	// 找到父节点
		// 当尚未达到顶端, 且父节点的值小于新值(不符合max-heap的次序特性)
		while (holeIndex > topIndex && *(first + parent) < value) {
			*(first + holeIndex) = *(first + parent); // 移动父值到洞号处
			holeIndex = parent; // 调整洞号为父节点
			parent = (holeIndex - 1) / 2; // 新洞的父节点
		}  // 循环到顶端,或者满足max-heap的顺序为止
		*(first + holeIndex) = value; // 将新值放入循环完得到的洞号,完成push操作
}

再回头看 __partial_sort 算法,当*i < *first时,代码中没有互换i所指元素和first所指元素。实际上这一步操作是在 __pop_heap 函数 完成的,在该函数内,先将要first元素放入指定的result,然后用新值value去调整max-heap。

template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,
                       RandomAccessIterator result, T value, Distance*) {
  *result = *first; // 将heap顶端元素值放入 result中
  // 重新调整heap,洞号为0,欲调整的新值为value
  __adjust_heap(first, Distance(0), Distance(last - first), value);
}

最后来看看 sort_heap 算法,该算法将[first, last)中的元素由堆序变为增序排列。每次弹出堆的最大值并放入尾部,然后缩小堆的范围,循环执行弹出操作直至堆只剩下最后一个元素。


template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {
	while (last - first > 1)  // 直到只剩一个元素为止
		pop_heap(first, last--); // 每执行一次,范围缩小一格
}

template <class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) {
	__pop_heap_aux(first, last, value_type(first));
}
 
template <class RandomAccessIterator, class T>
inline void __pop_heap_aux(RandomAccessIterator first,
	RandomAccessIterator last, T*) {
		// 将first元素值(即最大值)放入last-1,然后重调[first, last-1)为max-heap
		__pop_heap(first, last-1, last-1, T(*(last-1)), distance_type(first));
}

参考文章:

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐