1. 程式人生 > >Algorithm——一般陣列元素選擇問題(十一)

Algorithm——一般陣列元素選擇問題(十一)

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)小的了
	}
}