目录
【案例1 利用快排的partition过程,BFPRT】
【题目描述】
【思路解析】
【代码实现】
【案例2 动态规划的斜率优化技巧】
【题目描述】
【思路解析】
【代码实现】
【案例3 二叉树的递归套路】
【题目描述】
【搜索二叉树定义】
【思路解析】
【代码实现】
【案例4 完美洗牌问题】
【题目描述】编辑
【思路解析】
【代码实现】
【案例5 完美洗牌问题的应用】
【题目描述】
【思路解析】
【代码实现】
要求时间复杂度为O(N)。k指的是它排序后在数组中的索引。
利用随机快排的思路,随机选择一个数值x。然后将整个无序数组分为小于x的区域,等于x的区域,大于x的区域。
(1)如果k索引落在等与x的区域中,则返回x。
(2)如果k索引小于x的区域的左边界,对小于x的区域进行随机快排的partition过程。
(3)如果k索引大于x的区域的右边界,对大于x的区域进行随机快排的partition过程。
BFPRT
将整个数组分为以五个为一组的小数组,然后求出小数组的中位数,将这些所有小数组的中位数组成一个数组,然后求出这个数组的中位数(求这个数组中位数时,通过递归完成)。根据这个中位数进行随机快排的partition过程,其余过程和上一个方案相似。只是BFPRT在选取分区数值时能保证分区的3个区域较均匀。
这里只给出BFPRT的代码,两个代码思路相似,并且随机快排实现更为简单。
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;
}
}
因为1 2 1和1 1 2和2 1 1认为是同一种,所以我们就认为每次裂开的数一定要升序(并且第一次裂开的数字一定要大于等于1)。 这样就能保证方法不重复,并且得到所有方法。
通过这样的递归思路可以改为动态规划,使用动态规划的斜率优化技巧。
这里行索引表示先前裂开数,列索引表示当前剩余多少需要裂开。
0 | 0 | 0 | 0 | 0 | 0 |
1 | 1 | k | i | ||
1 | 0 | 1 1 | j | ||
1 | 0 0 | 0 | 1 | ||
1 0 | 0 | 0 | 0 | 1 | |
1 | 0 | 0 | 0 | 0 | 1 |
i表格的值根据上述依赖关系可知,它依赖红色数值。
j表格的值根据依赖关系可知,它依赖蓝色数值。
则i位置可以优化为依赖j位置和k位置。
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];
}
}
搜索二叉树(Binary Search Tree,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 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 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; // 传回信息,让它上面的结点更新表结构。
}
}
}
因为 这个数组位置的交换是很明确的,即我们在一开始就可以通过简单的函数来确定它最后应该到达的位置,假设索引位置从1开始,则左边区域的i位置最后会去到2i位置,则右边区域的j位置最后会去到2*(j-n)-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)。
这里是论文:
然后我们考虑一个事:
我们怎样将
a b c d 1 2 3 ->> 改为 1 2 3 a b c d;
第一步两边逆序, d c b a 3 2 1
第二步整体逆序 1 2 3 a b c 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;
}
}
}
给定一个无序数组,如何在空间复杂度为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)如果数组长度为奇数,
L0 L1 L2 L3 L4 R1 R2 R3 R4.除去第一个,使用完美洗牌。
变为L0 R1 L1 R2 L2 R3 L3 R4 L4.整体为<= >= <= >= <=满足题目要求。
堆排序请看看博客详解堆排序和桶排序和排序大总结_Studying~的博客-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;
}
}
}
}