Zuo Shen Advanced Advanced クラス 6 (クイック ソートの分割プロセス、BFPRT、動的プログラミングの傾き最適化スキル、バイナリ ツリーの再帰ルーチン、完全シャッフル問題の使用)

目次

【ケース1 クイックソートBFPRTの分割処理を利用する場合】

[タイトル説明]

 【アイデアの分析】

【コード】

【事例2 動的計画法の傾き最適化手法】

[タイトル説明]

【アイデアの分析】

【コード】

【事例3 二分木の再帰ルーチン】 

[タイトル説明]

 [二分木定義の検索]

 【アイデアの分析】

【コード】

【ケース4 完全シャッフル問題】

【タイトル説明】編集

 【アイデアの分析】

【コード】

【ケース5 完全シャッフル問題の応用】

[タイトル説明]

【アイデアの分析】

【コード】


うまく書けると思われる場合は、QQ グループ 907575059 に参加して、アルゴリズムの知識について話し合うことができます。

【ケース1 クイックソートBFPRTの分割処理を利用する場合】

[タイトル説明]

必要な時間計算量は O(N) です。k は、ソート後の配列内のインデックスを参照します。

 【アイデアの分析】

ランダムクイックソートのアイデアを使用して、値 x をランダムに選択します。次に、順序なし配列全体を x より小さい領域、x に等しい領域、および x より大きい領域に分割します。

(1) k インデックスが x に等しい領域にある場合、x を返します。

(2) k インデックスが x より小さい領域の左境界より小さい場合、x より小さい領域に対してランダム クイック ソート分割処理を実行します。

(3) k インデックスが x より大きい領域の右境界より大きい場合、x より大きい領域に対してランダム クイック ソート分割処理を実行します。

BFPRT

配列全体を 5 つの小さな配列に分割し、その小さな配列の中央値を見つけます。これらすべての小さな配列の中央値を 1 つの配列に結合して、この配列の中央値を見つけます (配列の中央値を取得する場合、これを見つけます)。それは再帰的に行われます)。ランダム クイック ソートの分割プロセスはこの中央値に基づいて実行され、プロセスの残りの部分は前のソリューションと同様です。BFPRT は、パーティション値を選択するときに、パーティションの 3 つの領域が相対的に均一であることを保証できるだけです。

 ここでは BFPRT コードのみを示しますが、2 つのコードは同様の考え方を持ち、ランダム クイック ソートの実装はより簡単です。

【コード】

package AdvancedPromotion6;


/**
 * @ProjectName: study3
 * @FileName: Ex1
 * @author:HWJ
 * @Data: 2023/9/25 21:51
 */
public class Ex1 {
    public static void main(String[] args) {
       int[] arr = {2,4,7,6,5,3,1,8};
        System.out.println(select(arr,0,arr.length - 1, 0));

    }
    
    public static int select(int[] arr, int begin, int end, int i) {
        if (begin == end) {
            return arr[begin];
        }
        int pivot = medianOfMedians(arr, begin, end);
        int[] pivotRange = partition(arr, begin, end, pivot);
        if (i >= pivotRange[0] && i <= pivotRange[1]) {
            return arr[i];
        } else if (i < pivotRange[0]) {
            return select(arr, begin, pivotRange[0] - 1, i);
        } else {
            return select(arr, pivotRange[1] + 1, end, i);
        }
    }

    public static int medianOfMedians(int[] arr, int begin, int end) {
        int num = end - begin + 1;
        int offset = num % 5 == 0 ? 0 : 1;
        int[] mArr = new int[num / 5 + offset];
        for (int i = 0; i < mArr.length; i++) {
            int beginI = begin + i * 5;
            int endI = beginI + 4;

            // 这里数组长度为5,使用插入排序的时间很低。
            mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
        }
        return select(mArr, 0, mArr.length - 1, mArr.length / 2);
    }

    public static int getMedian(int[] arr, int i, int j) {
        insertionSort(arr, i, j);
        return arr[(i + j) / 2];
    }

    public static void insertionSort(int[] arr, int begin, int end) {
        for (int i = begin + 1; i != end + 1; i++) {
            for (int j = i; j != begin; j--) {
                if (arr[j - 1] > arr[j]) {
                    swap(arr, j - 1, j);
                } else {
                    break;
                }
            }
        }
    }

    // 返回等于区域的左右边界
    public static int[] partition(int[] arr, int begin, int end, int num) {
        int p1 = begin - 1;
        int p2 = end + 1;
        int index = begin;
        int[] data = new int[2];
        while (index < p2) {
            if (arr[index] < num) {
                swap(arr, index++, ++p1);
            } else if (arr[index] > num) {
                swap(arr, index, --p2);
            } else {
                index++;
            }
        }
        data[0] = p1 + 1;
        data[1] = p2 - 1;
        return data;
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

【事例2 動的計画法の傾き最適化手法】

[タイトル説明]

【アイデアの分析】

1 2 1 と 1 1 2 および 2 1 1 は同じ種類とみなされているため、各分割の番号は昇順である必要があると考えられます (最初の分割の番号は 1 以上である必要があります)。 。これにより、メソッドが繰り返されず、すべてのメソッドが取得されます。

この再帰的なアイデアは、動的プログラミングの傾き最適化手法を使用して動的プログラミングに変更できます。

ここで、行インデックスは以前の分割の数を表し、列インデックスは分割されていない現在の数を表します。

0 0 0 0 0 0
1 1 k
1 0 1    1 j
1 0  0 1
1    0 0 0 0 1
1 0 0 0 0 1

i テーブルの値は上記の依存関係からわかり、赤色の値に依存します。

j テーブルの値は、青い値に依存する依存関係に従って確認できます。

次に、j 位置と k 位置に応じて i 位置を最適化できます。

【コード】

package AdvancedPromotion6;


/**
 * @ProjectName: study3
 * @FileName: Ex2
 * @author:HWJ
 * @Data: 2023/9/25 22:52
 */
public class Ex2 {
    public static void main(String[] args) {
        System.out.println(getNum(1, 4));
        System.out.println(dpWay(4));
    }

    // rest表示当前剩下多少需要用来分裂。
    // pre表示之前分裂了多少。
    public static int getNum(int pre, int rest){
        if (rest == 0){
            return 1;
        }
        if (pre > rest){
            return 0;
        }
        int res = 0;
        for (int i = pre; i <= rest; i++) {
            res += getNum(i, rest - i);
        }
        return res;
    }

    public static int dpWay(int num){
        if (num < 1) {
            return 0;
        }
        int[][] map = new int[num + 1][num + 1];
        for (int i = 1; i < num + 1; i++) {
            map[i][0] = 1;
        }
        for (int i = 1; i < num + 1; i++) {
            map[i][i] = 1;
        }
        for (int i = num - 1; i > 0; i--) {
            for (int j = i + 1; j < num + 1; j++) {
                map[i][j] = map[i][j - i] + map[i + 1][j];
            }
        }
        return map[1][num];
    }
    
}

【事例3 二分木の再帰ルーチン】 

[タイトル説明]

 [二分木定義の検索]

二分探索木 (BST) は、各ノードにキーが含まれる二分木であり、各ノードのキーは、左側のサブツリーのどのノードのキーよりも大きく、右側のサブツリーのどのキーよりも小さいです。 。つまり、検索バイナリ ツリー内のどのノードについても、そのノードの左側のサブツリーにあるすべてのキーワードの値がノードのキーワードの値よりも小さく、同時にすべてのキーワードの値も小さくなります。ノードの右側のサブツリー内のキーワードがノード キーの値より大きい、単語の値。このソート方法により、検索バイナリ ツリーを高速な検索、挿入、削除、その他の操作に使用できるようになります。

 【アイデアの分析】

 二分木の検索条件を満たす最大のトポロジのサイズを返したいためです。トポロジはサブツリーではないため、このツリーがバイナリ ツリーを検索するための条件を満たすようにするために、一部のサブツリーを破棄することができます。したがって、トポロジをボトムアップで更新できます。

【コード】

package AdvancedPromotion6;

import java.util.HashMap;

/**
 * @ProjectName: study3
 * @FileName: Ex3
 * @author:HWJ
 * @Data: 2023/9/26 10:31
 */
public class Ex3 {
    public static void main(String[] args) {

    }

    public static class Node {
        Node left;
        Node right;
        int value;

        public Node(int value) {
            this.value = value;
        }
    }

    public static class Record {
        int left;
        int right;

        public Record(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }


    public static int posOrder(Node head, HashMap<Node, Record> map){
        if (head == null){
            return 0;
        }
        int leftAns = posOrder(head.left, map); // 得到左孩子的答案
        int rightAns = posOrder(head.right, map); // 得到右孩子的答案
        modifyMap(head.left, head.value, map, true); // 对左子树进行更新
        modifyMap(head.right, head.value, map, false);  // 对右子树进行更新
        Record left = map.get(head.left);
        Record right = map.get(head.right);
        int lBest = left == null ? 0 : left.left + left.right + 1;
        int rBest = right == null ? 0 : right.right + right.left + 1;
        Record record = new Record(lBest, rBest);
        map.put(head, record);
        return Math.max(lBest + rBest + 1, Math.max(leftAns, rightAns));
    }

    // s == true, 查询左子树的右边界    s == false, 查询右子树的左边界。
    public static int modifyMap(Node node, int value, HashMap<Node, Record> map, boolean s) {
        if (node == null || !map.containsKey(node)) {
            return 0;
        }
        Record record = map.get(node);
        if ((s && node.value > value) || (!s && node.value < value)) {
            map.remove(node);
            return record.left + record.right + 1; // 传回信息,让它上面的结点更新表结构。
            // 如果第一次进来就不满足搜索二叉树条件,则直接删除结构,然后在调用处进行更新表结构。
        } else {
            int minus = modifyMap(s ? node.right : node.left, value, map, s);
            if (s) {
                record.right = record.right - minus;
            } else {
                record.left -= minus;
            }
            map.put(node, record); // 更新表结构
            return minus; // 传回信息,让它上面的结点更新表结构。
        }

    }
}

【ケース4 完全シャッフル問題】

[タイトル説明]

 【アイデアの分析】

配列位置の交換は非常に明確であるため、つまり、単純な関数を使用して、最初に到達する最終位置を決定できます。インデックス位置が 1 から始まると仮定すると、最終的に左側の領域の i 位置が2i の位置に移動すると、右側の領域の j の位置は最終的に 2*(jn)-1 の位置に移動します。(最終的にはすべてのインデックスを (2*i)% (len + 1) として統一できます)。私たちの最も独創的なアイデアは、(最終的に到達するインデックス位置を取得することで) ドミノ倒しのようにプッシュし続けることができるかどうかです。しかし、配列の長さによってはそのような効果を満たすことができることがわかりましたが、配列の長さによっては、いくつかの配列が形成されることがわかります。ドミノのようなサークル。

神聖な結論:

ここで、論文「A Simple In-Place Algorithm for In-Shuffle」からの結論を引用したいと思います。つまり、長さ 2*n = (3^k-1) の配列の場合、ちょうど k 個の円が存在します。各円の頭の開始位置は 1,3,9,...3^(k-1) です。

論文は次のとおりです。

次に、次のことを考えます。

どうやって

abcd 1 2 3 ->> 1 2 3 abcd に変更されました。

最初のステップは、両側の順序を逆にすることです (dcba 3 2 1)。

2 番目のステップは、全体的に逆の順序 1 2 3 abc d です。

結論によれば、これらの特殊な配列長については有効な解が得られますが、一般的な偶数については有効な解がありません。ただし、特別な長さに分割することもできます。長さを14とすると、8・2・2・2に分割できます。

また、L1 L2 L4 L4 R1 R2 R3 R4 L5 R5 L6 R6 L6 R7 は、複数の逆シーケンスによって実現できます。次に、これらの特別な配列を完全にシャッフルするだけです。

【コード】

package AdvancedPromotion6;

/**
 * @ProjectName: study3
 * @FileName: Ex4
 * @author:HWJ
 * @Data: 2023/9/26 11:22
 */
public class Ex4 {
    public static void main(String[] args) {

    }

    public static int modifyIndex(int i, int len) {
        if (len <= len / 2) {
            return 2 * i;
        } else {
            return 2 * (i - len) - 1;
        }
    }

    public static int modifyIndex2(int i, int len) {
        return (2 * i) % (len + 1);
    }

    public static void shuffle(int[] arr) {

        // (arr.length & 1) == 0限制数组长度不能为奇数。
        if (arr != null && arr.length != 0 && (arr.length & 1) == 0) {
            shuffle(arr, 0, arr.length - 1);
        }
    }

    public static void shuffle(int[] arr, int l, int r) {
        while (r - l + 1 > 0) {
            int len = r - l + 1;
            int base = 3;
            int k = 1;

            // base <= (len + 1) / 3,这样的循环条件能保证循环终止后,3^(k-1) - 1 <= len中最大的
            while (base <= (len + 1) / 3) {
                base *= 3;
                k++;
            }
            int half = (base - 1) / 2;
            int mid = (l + r) / 2;
            rotate(arr, l + half, mid,  mid + 1, mid + half);
            cycles(arr, l, base - 1, k);
            l = l + base - 1;
        }
    }

    public static void rotate(int[] arr, int p1, int p2, int q1, int q2) {
        reverse(arr, p1, p2);
        reverse(arr, q1, q2);
        reverse(arr, p1, q2);
    }

    public static void reverse(int[] arr, int p, int q) {
        while (p < q) {
            int tmp = arr[p];
            arr[p++] = arr[q];
            arr[q--] = tmp;
        }
    }

    public static void cycles(int[] arr, int start, int len, int k){
        for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
            int preValue = arr[start + trigger - 1];
            int cur = modifyIndex2(trigger, len);
            while (cur != trigger){
                int tmp = arr[start + cur - 1];
                arr[start + cur - 1] = preValue;
                preValue = tmp;
                cur = modifyIndex2(cur, len);
            }
            arr[cur + start - 1] = preValue;
        }
    }
}

【ケース5 完全シャッフル問題の応用】

[タイトル説明]

順序付けされていない配列が与えられた場合、空間複雑度 O(1) を達成するにはどうすればよいでしょうか?

a[0] <= a[1],a[1]>=a[2],a[2] <=a[3],a[3]>=a[4] を満たすように配列を変更します。 .....注文。

【アイデアの分析】

O(1) の空間計算量で完了する必要があるため、ヒープ ソートのみを使用できます。

ヒープソートを使用すると、配列全体が全体的に増加します。

(1) 配列長が偶数の場合、L1 L2 L3 L4 R1 R2 R3 R4。

パーフェクト シャッフルを使用すると、R1 L1 R2 L2 R3 L3 R4 L4 となり、全体的な結果は >= <= >= <= になります。

しかし、2人全員がグループであれば、それは達成できます。

(2) 配列の長さが奇数の場合、

L0 L1 L2 L3 L4 R1 R2 R3 R4. 最初のものを削除し、パーフェクト シャッフルを使用します。

L0 R1 L1 R2 L2 R3 L3 R4 L4 となり、全体は <= >= <= >= <= となり、質問の要件を満たします。

【コード】

ヒープ ソートについては、ヒープ ソート、バケット ソートの詳細な説明と、sorting_Studing~ のブログ - CSDN ブログの概要についてのブログをお読みください。

package class05;

import java.util.Arrays;

public class Problem04_ShuffleProblem {

	// 数组的长度为len,调整前的位置是i,返回调整之后的位置
	// 下标不从0开始,从1开始
	public static int modifyIndex1(int i, int len) {
		if (i <= len / 2) {
			return 2 * i;
		} else {
			return 2 * (i - (len / 2)) - 1;
		}
	}

	// 数组的长度为len,调整前的位置是i,返回调整之后的位置
	// 下标不从0开始,从1开始
	public static int modifyIndex2(int i, int len) {
		return (2 * i) % (len + 1);
	}

	// 主函数
	// 数组必须不为空,且长度为偶数
	public static void shuffle(int[] arr) {
		if (arr != null && arr.length != 0 && (arr.length & 1) == 0) {
			shuffle(arr, 0, arr.length - 1);
		}
	}

	// 在arr[L..R]上做完美洗牌的调整
	public static void shuffle(int[] arr, int L, int R) {
		while (R - L + 1 > 0) { // 切成一块一块的解决,每一块的长度满足(3^k)-1
			int len = R - L + 1;
			int base = 3;
			int k = 1;
			// 计算小于等于len并且是离len最近的,满足(3^k)-1的数
			// 也就是找到最大的k,满足3^k <= len+1
			while (base <= (len + 1) / 3) {
				base *= 3;
				k++;
			}
			// 当前要解决长度为base-1的块,一半就是再除2
			int half = (base - 1) / 2;
			// [L..R]的中点位置
			int mid = (L + R) / 2;
			// 要旋转的左部分为[L+half...mid], 右部分为arr[mid+1..mid+half]
			// 注意在这里,arr下标是从0开始的
			rotate(arr, L + half, mid, mid + half);
			// 旋转完成后,从L开始算起,长度为base-1的部分进行下标连续推
			cycles(arr, L, base - 1, k);
			// 解决了前base-1的部分,剩下的部分继续处理
			L = L + base - 1;
		}
	}

	// 从start位置开始,往右len的长度这一段,做下标连续推
	// 出发位置依次为1,3,9...
	public static void cycles(int[] arr, int start, int len, int k) {
		// 找到每一个出发位置trigger,一共k个
		// 每一个trigger都进行下标连续推
		// 出发位置是从1开始算的,而数组下标是从0开始算的。
		for (int i = 0, trigger = 1; i < k; i++, trigger *= 3) {
			int preValue = arr[trigger + start - 1];
			int cur = modifyIndex2(trigger, len);
			while (cur != trigger) {
				int tmp = arr[cur + start - 1];
				arr[cur + start - 1] = preValue;
				preValue = tmp;
				cur = modifyIndex2(cur, len);
			}
			arr[cur + start - 1] = preValue;
		}
	}

	// [L..M]为左部分,[M+1..R]为右部分,左右两部分互换
	public static void rotate(int[] arr, int L, int M, int R) {
		reverse(arr, L, M);
		reverse(arr, M + 1, R);
		reverse(arr, L, R);
	}

	// [L..R]做逆序调整
	public static void reverse(int[] arr, int L, int R) {
		while (L < R) {
			int tmp = arr[L];
			arr[L++] = arr[R];
			arr[R--] = tmp;
		}
	}

	public static void wiggleSort(int[] arr) {
		if (arr == null || arr.length == 0) {
			return;
		}
		// 假设这个排序是额外空间复杂度O(1)的,当然系统提供的排序并不是,你可以自己实现一个堆排序
		Arrays.sort(arr);
		if ((arr.length & 1) == 1) {
			shuffle(arr, 1, arr.length - 1);
		} else {
			shuffle(arr, 0, arr.length - 1);
			for (int i = 0; i < arr.length; i += 2) {
				int tmp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = tmp;
			}
		}
	}

	// for test
	public static boolean isValidWiggle(int[] arr) {
		for (int i = 1; i < arr.length; i++) {
			if ((i & 1) == 1 && arr[i] < arr[i - 1]) {
				return false;
			}
			if ((i & 1) == 0 && arr[i] > arr[i - 1]) {
				return false;
			}
		}
		return true;
	}

	// for test
	public static void printArray(int[] arr) {
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	// for test
	public static int[] generateArray() {
		int len = (int) (Math.random() * 10) * 2;
		int[] arr = new int[len];
		for (int i = 0; i < len; i++) {
			arr[i] = (int) (Math.random() * 100);
		}
		return arr;
	}

	public static void main(String[] args) {
		for (int i = 0; i < 5000000; i++) {
			int[] arr = generateArray();
			wiggleSort(arr);
			if (!isValidWiggle(arr)) {
				System.out.println("ooops!");
				printArray(arr);
				break;
			}
		}
	}

}

 

おすすめ

転載: blog.csdn.net/weixin_73936404/article/details/133267184
おすすめ