Algorithm——一般数组元素选择问题(十一)

本文介绍了一种基于快速排序的随机化子数组划分方法来解决数组元素选择问题的算法。通过递归地选择一个随机主元对数组进行划分,直至找到第i小的元素。

Algorithm——一般数组元素选择问题


《算法导论》中引出了一个一般性的数组元素选择问题:即在一个元素各异的数组A中,选择其中第i小的元素(即如果i=1,则表明选择最小的那个元素)。

该算法的伪代码如下,它使用了之前介绍快速排序中的随机子数组划分方法:

RANDOMIZED-SELECT(A, p, r, i)
    if p == r
        return A [p]
    q = RANDOMIZED-PARTITION(A, p, r)
    k = q - p + 1
    if i == k
        return A[q]
    else if i < k
        return RANDOMIZED-SELECT(A, p, q - 1, i)
    else
        return RANDOMIZED-SELECT(A, q + 1, r, i - k)
此算法的Java实现如下:

	/**
	 * 
	 * 在给定的数组下标区间范围内,根据选定的主元划分子数组
	 * 
	 * @param A
	 *            待排序的主数组
	 * @param p
	 *            子数组下标的左边界
	 * @param r
	 *            子数组下标的右边界
	 * @return 得到的子数组划分分界下标值
	 */
	private int partition(int[] A, int p, int r) {
		int pivot = A[r];// pivot element
		int i = p - 1;
		for (int j = p; j < r; j++) {
			// 当pivot >=
			// A[j]时,下标为j的元素就是需要与下标为i的元素进行交换的元素;下表为i的元素是前面那些比pivot大的元素的下标;即,在发现一个元素比pivot小时,就将此元素与之前的比pivot元素大的元素进行交换;这样比pivot小的元素总会出现在数组左边,并连续出现.
			if (A[j] <= pivot) {
				i++;// 下标自加1;每次自加1后,当前下标i指向的元素都比pivot大
				exchange(A, i, j);
			}
		}
		// for循环结束后此时数组A[p...r]的状态是:
		// 对于任意下标k,如果k在[p,i]区间内,总有A[k] <= pivot;
		// 对于任意下标k,如果k在[i+1,j]区间内,总有A[k] > pivot;
		// 若下标k = r,则A[r] = pivot;
		exchange(A, i + 1, r);// 将A[i+1]与A[r]交换,此时会有小远(i+1)下标的元素的值都小于等于pivot;大于(i+1)下标的元素值都大于pivot
		return i + 1;
	}

	// 用于随机抽样
	private Random util = new Random();

	/**
	 * 
	 * 加入随机抽样的数组划分算法
	 * 
	 * @param A
	 *            待排序的数组A
	 * @param p
	 *            待排序的子数组下标左边界
	 * @param r
	 *            带排序的子数组下标右边界
	 * @return 得到的子数组划分分界下标值
	 */
	public int randomizedPartition(int[] A, int p, int r) {
		int i = util.nextInt(r - p + 1)/* 区间[0,r-p+1) */ + p;// 随机生成一个在区间[p,r]之间的下标值
		exchange(A, i, r);// 保证子数组划分所需要的pivot element是随机从A[p...r]中选取的
		return partition(A, p, r);// 调用常规子数组划分函数,进行子数组划分
	}

	/**
	 * 一种解决选择问题的分治算法:返回数组A(不包含相同元素)中下标范围[p...r]内的第i小的元素;i从1开始算,i=1则说明选择A[p...r]中最小的那个元素
	 * 
	 */
	public int randomizedSelect(int[] A, int p, int r, int i) {
		if (p == r)
			return A[p];//如果指定的下标范围内只有一个元素,那当前元素就是所需要的第i小的元素
		int q = randomizedPartition(A, p, r);//使用随机子数组划分方法,借助一个随机主元去划分子数组(可能为空);返回的q的下标是当前主元的下标
		//在上一步划分完子数组后,A[p...r]实际上可以看做被划分成了三个子数组区间:A[p...q-1] =< A[q] < A[q+1...r];此时主元A[q]在整个子数组A[p...r]中已经是排好序的了
		int k = q - p + 1;//计算子数组A[p...q]内的元素个数
		if (i == k)//如果i==k,则说明此次的主元A[q]就是要选择的第i小的元素,此时就直接返回;否则下面两步就要确定要选的元素是在子数组A[p...q-1]还是在A[q+1...r]中了
			return A[q];
		else if (i < k)//i < k,说明待选择的元素在子数组中A[p...q-1]中,随后继续递归查找
			return randomizedSelect(A, p, q - 1, i);
		else //i > k,说明待选择的元素在子数组A[q+1...r]中,随后继续递归查找;
			return randomizedSelect(A, q + 1, r, i - k);//同时要注意,此时我们已知了A[p...q]中的元素(总共有k个)都是比待选择的元素小的,那么在子数组A[q+1...r]中,待选择的元素应该是第(i-k)小的了
	}
完整的测试代码是:

public class SortAlgor {

	public static void main(String[] args) {

		int[] A = { 2, 5, 10, 8, 9, 12, 15, 4 };
		SortAlgor algorithm = new SortAlgor();
		System.out.println(algorithm.randomizedSelect(A, 0, A.length-1, 3));//返回5
	}

	/**
	 * 
	 * 在给定的数组下标区间范围内,根据选定的主元划分子数组
	 * 
	 * @param A
	 *            待排序的主数组
	 * @param p
	 *            子数组下标的左边界
	 * @param r
	 *            子数组下标的右边界
	 * @return 得到的子数组划分分界下标值
	 */
	private int partition(int[] A, int p, int r) {
		int pivot = A[r];// pivot element
		int i = p - 1;
		for (int j = p; j < r; j++) {
			// 当pivot >=
			// A[j]时,下标为j的元素就是需要与下标为i的元素进行交换的元素;下表为i的元素是前面那些比pivot大的元素的下标;即,在发现一个元素比pivot小时,就将此元素与之前的比pivot元素大的元素进行交换;这样比pivot小的元素总会出现在数组左边,并连续出现.
			if (A[j] <= pivot) {
				i++;// 下标自加1;每次自加1后,当前下标i指向的元素都比pivot大
				exchange(A, i, j);
			}
		}
		// for循环结束后此时数组A[p...r]的状态是:
		// 对于任意下标k,如果k在[p,i]区间内,总有A[k] <= pivot;
		// 对于任意下标k,如果k在[i+1,j]区间内,总有A[k] > pivot;
		// 若下标k = r,则A[r] = pivot;
		exchange(A, i + 1, r);// 将A[i+1]与A[r]交换,此时会有小远(i+1)下标的元素的值都小于等于pivot;大于(i+1)下标的元素值都大于pivot
		return i + 1;
	}

	// 用于随机抽样
	private Random util = new Random();

	/**
	 * 
	 * 加入随机抽样的数组划分算法
	 * 
	 * @param A
	 *            待排序的数组A
	 * @param p
	 *            待排序的子数组下标左边界
	 * @param r
	 *            带排序的子数组下标右边界
	 * @return 得到的子数组划分分界下标值
	 */
	public int randomizedPartition(int[] A, int p, int r) {
		int i = util.nextInt(r - p + 1)/* 区间[0,r-p+1) */ + p;// 随机生成一个在区间[p,r]之间的下标值
		exchange(A, i, r);// 保证子数组划分所需要的pivot element是随机从A[p...r]中选取的
		return partition(A, p, r);// 调用常规子数组划分函数,进行子数组划分
	}

	/**
	 * 一种解决选择问题的分治算法:返回数组A(不包含相同元素)中下标范围[p...r]内的第i小的元素;i从1开始算,i=1则说明选择A[p...r]中最小的那个元素
	 * 
	 */
	public int randomizedSelect(int[] A, int p, int r, int i) {
		if (p == r)
			return A[p];//如果指定的下标范围内只有一个元素,那当前元素就是所需要的第i小的元素
		int q = randomizedPartition(A, p, r);//使用随机子数组划分方法,借助一个随机主元去划分子数组(可能为空);返回的q的下标是当前主元的下标
		//在上一步划分完子数组后,A[p...r]实际上可以看做被划分成了三个子数组区间:A[p...q-1] =< A[q] < A[q+1...r];此时主元A[q]在整个子数组A[p...r]中已经是排好序的了
		int k = q - p + 1;//计算子数组A[p...q]内的元素个数
		if (i == k)//如果i==k,则说明此次的主元A[q]就是要选择的第i小的元素,此时就直接返回;否则下面两步就要确定要选的元素是在子数组A[p...q-1]还是在A[q+1...r]中了
			return A[q];
		else if (i < k)//i < k,说明待选择的元素在子数组中A[p...q-1]中,随后继续递归查找
			return randomizedSelect(A, p, q - 1, i);
		else //i > k,说明待选择的元素在子数组A[q+1...r]中,随后继续递归查找;
			return randomizedSelect(A, q + 1, r, i - k);//同时要注意,此时我们已知了A[p...q]中的元素(总共有k个)都是比待选择的元素小的,那么在子数组A[q+1...r]中,待选择的元素应该是第(i-k)小的了
	}
}









### C++ 中数组元素排序 在 C++ 中,可以利用标准库 `<algorithm>` 提供的 `std::sort` 函数对数组进行高效排序。此函数采用快速排序算法的一个变种,平均情况下具有 O(n log n) 的时间复杂度。 #### 排序一维整型数组示例 ```cpp #include <iostream> #include <algorithm> // 导入 sort 所需头文件 int main() { int arr[] = {5, 2, 9, 1, 5, 6}; size_t length = sizeof(arr)/sizeof(*arr); // 计算数组长度 std::cout << "原始数组:" << std::endl; for(size_t i=0; i<length; ++i){ std::cout << arr[i] << ' '; } std::sort(std::begin(arr), std::end(arr)); // 对整个数组范围内的元素进行升序排列[^1] std::cout << "\n排序后的数组:" << std::endl; for(auto element : arr){ // 使用基于范围的for循环遍历容器或数组 std::cout << element << ' '; } return 0; } ``` 上述代码片段展示了如何定义一个简单的整形数组,并调用 `std::sort()` 来对其进行排序。注意这里使用了两个辅助宏——`std::begin()` 和 `std::end()` ——它们分别返回指向数组首尾位置的迭代器,从而指定要排序的数据区间。 对于更复杂的场景比如自定义对象组成的数组,则可能需要提供额外参数给 `std::sort` ,即第三个参数作为比较准则谓词(predicate),用于规定特定类型的比较逻辑。 #### 关于多维数组排序注意事项 当涉及到二维或多维数组时,由于其内部结构较为特殊,直接应用 `std::sort` 可能不会达到预期效果。如果目标是对每一行单独排序,那么应该逐行列出并独立处理;而若是想要按照某些规则整体重排这些子数组的位置,则通常先将其转换成适合单次排序的形式再执行相应操作[^4]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值