左神数据结构与算法(基础提升)——03

9 树形dp套路

树形dp使用前提:如果题目求解目标是S规则,则求解流程可以定成以每一个节点为头节点的子树在S规则下的每一个答案,并且最终答案一定在其中。

树形dp套路第一步

    以某个节点X为头节点的子树中,分析答案有哪些可能性,并且这种分析是以X的左子树、X的右子树和X整棵树的角度来考虑可能性的

树形dp套路第二步

    根据第一步的可能性分析,列出所有需要的信息

树形dp套路第三步

    合并第二步的信息,对左子树和右树提出同样的要求,并写出信息结构

树形dp套路第四步

    设计递归函数,递归函数是处理以X为头节点的情况下的答案。

    包括设计递归的basecase,默认直接得到左树和右树的所有信息,以及把可能性做整合,并且要返回第三步的信息结构着四个小步骤。

问题1:二叉树节点间的最大距离问题

从二叉树的节点a出发,可以向上或者向下走,但沿途的节点只能经过一次,到达节点b时路径上的节点个数叫做a到b的距离,那么二叉树任何两个节点之间都有距离,求整棵树上的最大距离。

public class MaxDistance {

    public static class Node{
        int value;
        Node left;
        Node right;
        
        public Node(int value){
            this.value = value;
        }
    }
    
    public static int getMaxDistance(Node node){
        return process(node).maxDistance;
    }
    
    //对于左子树和右子树需要知道的信息:最大距离、树的高度
    public static class Info{
        int maxDistance;  //和节点个数有关
        int height;
        
        public Info(int maxDistance,int height){
            this.maxDistance = maxDistance;
            this.height = height;
        }
    }
    
    public static Info process(Node node){
        if(node == null){
            return new Info(0,0);
        }
        Info leftInfo = process(node.left);
        Info rightInfo = process(node.right);
        //当前节点的信息应该怎么处理
        int p1 = leftInfo.maxDistance;
        int p2 = rightInfo.maxDistance;
        int p3 = leftInfo.height + rightInfo.height + 1; //经过当前节点的距离
        int maxDistance = Math.max(p3,Math.max(p1,p2));
        int height = Math.max(leftInfo.height,rightInfo.height) + 1;
        
        return new Info(maxDistance,height);
    }
}

问题2:派对的最大快乐值

员工信息的定义如下:

class Employee{

    public int happy;//这名员工可以带来的快乐值

    List<Employee> subordinates;//这名员工有哪些直接下级

}

公司的每个员工都符合Employee类的描述,整个公司的人员结构可以看作是一颗标准的。没有环的多叉树。树的头节点是公司的唯一老板。出老板之外的每个员工都有唯一的直接上级。叶节点是没有任何下属的基层员工(subordinates列表为空),出基层员工之外,每个员工都有一个或多个直接下级。

这个公司现在要办party,你可以决定哪些员工来,那些员工不来。但是要遵循以下规则。

1.如果某个员工来了,那么这个员工的所有直接下属都不能来

2.派对的整体快乐值是所有到场员工快乐值的累加

3.你的目标是让派对的整体快乐值尽量大

给定一颗多叉树的头节点boss,请返回派对的最大快乐值。

/**
 * 派对的最大快乐值
 */
public class MaxPartyHappy {

    public static class Employee{
        public int happy;//这名员工可以带来的快乐值
        List<Employee> subordinates;//这名员工有哪些直接下级
        
        public Employee(int happy){
            this.happy = happy;
        }
    }
    
    public static int getPartyMaxHappy(Employee boss){
        Info info = process(boss);
        return Math.max(info.laiMaxHappy,info.buMaxHappy);
    }
    
    //参与和不参与对于X节点都需要获得的信息
    public static class Info{
        public int laiMaxHappy;
        public int buMaxHappy;
        
        public Info(int laiMaxHappy,int buMaxHappy){
            this.laiMaxHappy = laiMaxHappy;
            this.buMaxHappy = buMaxHappy;
        }
    }
    
    public static Info process(Employee emp){
        if(emp == null){
            return new Info(0,0);
        }
        int laiHappy = emp.happy; //emp参与时,整棵树的最大快乐值要包括其快乐值
        int buHappy = 0; //emp不参与,其快乐值为0
        for(Employee e : emp.subordinates){
            Info eInfo = process(e);        
            laiHappy += eInfo.buMaxHappy; //当前员工参与,其直接下级员工只能选择不参与下的最大的快乐值
            buHappy += Math.max(eInfo.laiMaxHappy,eInfo.buMaxHappy);
        }
        return new Info(laiHappy,buHappy);
    }
}

10 二叉树的Morris遍历

一种遍历二叉树的方式,并且时间复杂度O(N),额外空间复杂度O(1)。

通过利用原树中大量空闲指针的方式,达到节省空间的目的。

Morris遍历细节

假设来到当前节点cur,开始时cur来到头节点位置

1>如果cur没有左孩子,cur向右移动(cur = cur.right)

2>如果cur有左孩子,找到左子树上最右的节点mostRight:

    a.如果mostRight的右指针为空,让其指向cur,然后cur向左移动(cur = cur.left)

    b.如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)

3>cur为空时遍历停止

public static class Node{
    int value;
    Node left;
    Node right;
    
    public Node(int value){
        this.value = value;
    }
}

public static void morris(Node head){
    if(head == null){
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while(cur != null){
        mostRight = cur.left;
        if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
            while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点 
                mostRight = mostRight.right;
            }
            //mostRight变成了左树上的最右节点
            if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
                mostRight.right = cur;
                cur = cur.left;
                continue;  //第一次来到过后,会直接进入下一次循环
            }else {  //第二次来到
                mostRight.right = null;
            }
        }
        cur = cur.right;
    }
}

可以根据morris遍历进行二叉树的先序、中序、后续遍历

先序遍历:节点只经过一次的,直接打印,经过两次的,打印第一次

public static void morris_Pre(Node head){ //先序遍历:节点只来一次,直接打印,来两次,打印第一次
    if(head == null){
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while(cur != null){
        mostRight = cur.left;
        if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
            while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
                mostRight = mostRight.right;
            }
            //mostRight变成了左树上的最右节点
            if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
                System.out.println(cur.value);
                mostRight.right = cur;
                cur = cur.left;
                continue;   //第一次来到过后,会直接进入下一次循环
            }else {  //第二次来到
                mostRight.right = null;
            }
        }else {
            System.out.println(cur.value);
        }
        cur = cur.right;
    }
}

中序遍历:节点只经过一次的,直接打印,经过两次的,打印第二次

public static void morris_Mid(Node head){ //中序遍历:节点只来一次,直接打印,来两次,打印第二次
    if(head == null){
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while(cur != null){
        mostRight = cur.left;
        if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
            while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
                mostRight = mostRight.right;
            }
            //mostRight变成了左树上的最右节点
            if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
                mostRight.right = cur;
                cur = cur.left;
                continue;  //第一次来到过后,会直接进入下一次循环
            }else {  //第二次来到
                mostRight.right = null;
            }
        }
        System.out.println(cur.value);
        cur = cur.right;
    }
}

后序遍历:打印第二次经过节点的左树的右边界(逆序打印)

public static void morris_Pos(Node head){ 
    if(head == null){
        return;
    }
    Node cur = head;
    Node mostRight = null;
    while(cur != null){
        mostRight = cur.left;
        if(mostRight != null){ //如果有左子树,就找到左子树的最右节点,没有则直接向右移动
            while (mostRight.right != null && mostRight.right != cur){ //找到左树上的最右节点
                mostRight = mostRight.right;
            }
            //mostRight变成了左树上的最右节点
            if(mostRight.right == null){ //第一次来到,使最右节点的右指针指向cur,然后cur继续左移找左子树的最右节点
                mostRight.right = cur;
                cur = cur.left;
                continue;  //第一次来到过后,会直接进入下一次循环
            }else {  //第二次来到
                mostRight.right = null;
                printEdge(cur.left);//打印第二次经过节点的左数的右边界(逆序)
            }
        }
        cur = cur.right;
    }
    printEdge(head);//整颗树跑完,单独打印整棵树左树的右边界(逆序)
    System.out.println();
}

//以node为头的树,逆序打印这棵树的右边界
public static void printEdge(Node node){
    Node tail = reverseEdge(node);
    Node cur = tail;
    while (cur != null){
        System.out.print(cur.value + "\t");
        cur = cur.right;
    }
    reverseEdge(tail);
}

public static Node reverseEdge(Node node){
    Node pre = null;
    Node next = null;
    while(node != null){
        next = node.right;
        node.right = pre;
        pre = node;
        node = next;
    }
    return pre;
}

11 大数据题目解题技巧

1)哈希函数可以把数据按照种类均匀分流

2)布隆过滤器用于集合的建立与查询,并可以节省大量空间

3)一致性哈希解决数据服务器的负载管理问题

4)利用并查集结构做岛问题的并行计算

5)位图解决某一范围上数字的出现情况,并可以节省大量空间

6)利用分段统计思想,并进一步节省大量空间

7)利用堆、外排序来做多个处理单元的结果合并

问题:32位无符号整数的范围使0~4294967295,现在有一个正好包含40亿个无符号整数的文件,所以在整数范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有未出现过的数?

0~2^32-1范围用位图表示,每一位表示范围内的一个值,然后出现过的数对应位图描黑即可。

[进阶]内存限制为10MB,但是只用找到一个没出现过的数即可。

    10KB 10000/4=2500  0~2^11范围

把无符号整数个数2^32划分出2^10个数,这样数的范围就是0~2^22-1,2^22~2*2^22-1,......;然后40亿个数进行划分,一定会出现有一个范围内不满的情况。在这个不满足的范围内继续划分......

问题:有一个包含100亿URL的大文件,假设每个URL占用64B,请找出其中所有重复的URL

    哈希、布隆过滤器

[补充]某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门Top词汇的可行办法。

    方法一:通过哈希,将这些词汇分配,并且重复发词频出现则加加。

    方法二:通过堆的方式,词汇还是根据哈希分类,将同一文件的词频数排列大根堆的方式,那么大根堆的堆顶就当前文件出现次数最多的词汇,再将每个大根堆的堆顶都取出放进总堆(各个大根堆最大的元素进行比较),再从总堆中输出即可。

问题:32位无符号整数的范围使0~4294967295,现在有一个正好包含40亿个无符号整数的文件,所以在整数范围中必然存在没出现过的数。可以使用最多1GB的内存,怎么找到所有出现了两次的数?

    哈希函数分流、位图(用两个位表示X出现的次数)

[补充]可以使用最多10MB的内存,怎么找到40亿个整数的中位数?

    10KB  整型数组一个占4字节,10KB/4=2.5KB=2500B  2^11=2048

    把无符号数2^32个根据给定的内存10KB划分出2048个范围,申请一个2048长度的数组。每个数组元素存放的是该范围内出现数的个数,然后找到数组元素累加和位20亿的范围,再此范围上进行寻找第20亿个数。

12 位运算题目

问题1:给定两个有符号32位整数a和b,返回a和b中较大的。[不用做任何判断]

public class getMaxNum {
    
    //保证参数n,不是1就是0的情况下
    //n = 1 -->  0
    //n = 0 -->  1
    public static int flip(int n){
        return n ^ 1;
    }
    
    //n是非负数,返回1
    //n是负数,返回0
    public static int sign(int n){
        return flip( (n >> 31) & 1 );  //取出n的符号位位
    }
    
    public static int getMax1(int a,int b){
        int c = a - b;   //可能会溢出
        int scA = sign(c);    //a-b非负,scA为 1;a-b为负,scA为 0
        int scB = flip(scA);  //scA 为0,scB为1; scA为1,scB为0
        //
        return a * scA + b * scB;
    }
    
    public static int getMax2(int a,int b){
        int c = a - b;
        int sa = sign(a);  //a的符号状态 ,非负为1,负数为0
        int sb = sign(b);
        int sc = sign(c);
        int difSab = sa ^ sb; //a和b的符号不一样,为1;一样,为0
        int sameSab = flip(difSab);  //a和b的符号一样,为1,不一样,为0
        int returnA = difSab * sa + sameSab * sc;
        int returnB = flip(returnA);
        return a * returnA + b * returnB;
    }
}

问题2:判断一个32位正数是不是2的幂、4的幂

public class twoorfourpower {
    
    //看这个数是不是只有一个1  
    // 2^0 = ...00000001;2^1 = ...00000010;2^2 = ...00000100;2^3 = ...00001000
    public static boolean is2Power(int n){
        return (  n & (n - 1)  ) == 0;//x&(x – 1)是将x的最后一个1置0
    }
    
    //一个数是4的幂一定先是2的幂
    public static boolean is4Power(int n){
                                             //......01010101 4的幂位是1
        return (  n & (n - 1)  ) == 0 && ( n & 0x55555555 ) != 0;
    }
}

问题3:给定两个有符号32位整数a和b,不能使用算术运算符,分别实现a和b的加、减、乘、除运算[如果给定a、b执行加减乘除的运算结果就会导致数据的溢出,那么你实现的函数不必对此负责,除此之外请保证计算过程不发生溢出]

public class jiajianchengchu {

    public static int add(int a,int b){
        int sum = a;
        while (b != 0){
            sum = a ^ b;  //相当于无进位相加
            b = (a & b) << 1;  //进位信息
            a = sum;
        }
        return sum;
    }

    public static int negNum(int n){
        return add(~n,1);
    }

    public static int minus(int a,int b){ //a - b = a + (-b)  b的相反数就是b取反加1
        return add(a,negNum(b));
    }

    public static int multi(int a,int b){
        int res = 0;
        while (b != 0){
            if((b & 1) != 0){
                res = add(res,a);
            }
            a <<= 1;  //左移
            b >>>= 1;  //右移,看最后一位是不是1
        }
        return res;
    }

    public static boolean isNeg(int n){
        return n < 0;
    }

    public static int div(int a,int b){  //b左移多少位(扩大倍数),可以减掉的话,对应位数位1
        //先把数都变为正数
        int x = isNeg(a) ? negNum(a) : a;
        int y = isNeg(b) ? negNum(b) : b;
        int res = 0;
        for(int i = 31;i > -1;i--){  //只能处理正数
            //右移比左移安全
            if((x >> i) >= y){ //x右移多少位还比y大,说明x至少是y的 2^i 倍  (y << i) <= x可能会有溢出的情况
                res |= (1 << i); //把对应位置为1
                x = minus(x,y << i);
            }
        }
        return isNeg(a) ^ isNeg(b) ? negNum(res) : res;
    }
}

13 暴力递归->动态规划

问题1:给定N个数,一个机器人在起点S,可以走K步,到达E点,问有多少种方法?

暴力递归->记忆化搜索->动态规划   一定要画表分析

public class Robot {

    //E:终点 S:起点  K:给定的步数
    public static int getWay1(int N,int E,int S,int K){
        return f1(N,E,K,S);
    }

    /**
     *
     * @param N     一共右1~N个位置,固定参数,不会改变的
     * @param E     最终要走到的位置  E  也是固定参数,不会改变的
     * @param rest  当前机器人还有多少步没有走
     * @param cur   当前机器人所在的位置
     * @return   返回方法数
     */
    public static int f1(int N,int E,int rest,int cur){
        if(rest == 0){  //判断走完给定的步数后,机器人是否到终点了,到了方法就加一,没到该路线就没有用
            return cur == E ? 1 : 0;
        }
        //rest > 0 机器人还没走完
        //在 1 的位置和 N 的位置机器人走向是固定的
        if(cur == 1){
            return f1(N,E,rest - 1,2);
        }
        if(cur == N){
            return f1(N,E,rest - 1,N - 1);
        }
        //不在端点的位置
        return f1(N,E,rest - 1,cur - 1) + f1(N,E,rest - 1,cur + 1);
    }

    //记忆化搜索:在原来的递归上加入缓存记录,这样走到的对应位置,直接拿方法数即可
    public static int getWay2(int N,int E,int S,int K){
        int[][] dp = new int[K+1][N+1];  //记录机器人走到不同位置的方法数
        for(int i = 0;i <= K;i++){  //将数组的只都初始化位-1
            for(int j = 0;j <= N;j++){
                dp[i][j] = -1;
            }
        }
        return f2(N,E,S,K,dp);
    }

    public static int f2(int N,int E,int rest,int cur,int[][] dp){
        if(dp[rest][cur] != -1){  //不是-1,说明该位置已被走过,直接拿方法数即可
            return dp[rest][cur];
        }
        //缓存没有命中
        if(rest == 0){
            dp[rest][cur] = cur == E ? 1 : 0;
            return dp[rest][cur];
        }
        //rest > 0 没走完
        if(cur == 1){
            dp[rest][cur] = f2(N,E,rest - 1,2,dp);
        }else if(cur == N){
            dp[rest][cur] = f2(N,E,rest - 1,N - 1,dp);
        }else {
            dp[rest][cur] = f2(N, E, rest - 1, cur - 1, dp) + f2(N, E, rest - 1, cur + 1, dp);
        }
        return dp[rest][cur];
    }

    //动态规划,根据递归结构对缓存数组进行分析,得到各个位置之间的关系
    public static int getWay3(int N,int E,int S,int K){
        int[][] dp = new int[K+1][N+1];  //dp[...][0]废了不用
        dp[0][E] = 1;  //终点的位置为 1
        for(int rest = 1;rest <= K;rest++){
            for(int cur = 1;cur <= N;cur++){
                if(cur == 1){
                    dp[rest][cur] = dp[rest - 1][2];
                }else if (cur == N){
                    dp[rest][cur] = dp[rest - 1][N - 1];
                }else {
                    dp[rest][cur] = dp[rest - 1][cur - 1] + dp[rest - 1][cur + 1];
                }
            }
        }
        return dp[K][S];  //根据原问题递归 f1(N,E,K,S) 可知最终要的是 K,S位置的值
    }
}

问题2:给定一组数,其中每个数代表一枚硬币,找到组成aim数的最少硬币

public class Coin {
    
    public static int getAim(int[] arr,int aim){
        return process(arr,0,aim);
    }

    /**
     * 
     * @param arr     硬币都在其中,固定参数
     * @param index   如果自由选择arr[index.......]这些硬币
     * @param rest    还有多少钱没有组成
     * @return     
     */
    public static int process(int[] arr,int index,int rest){
        if(rest < 0){
            return -1;
        }
        if(rest == 0){
            return 0;
        }
        //rest > 0
        if(index == arr.length){ //给定的硬币无法组成
            return -1;
        }
        //rest > 0 并且还有 硬币
        // 当前的硬币不选择  或者  选择
        // 取硬币数量最少的
        int p1 = process(arr,index+1,rest);
        int p2Next = process(arr,index+1,rest-arr[index]);
        if(p1 == -1 && p2Next == -1){
            return -1;
        }else {
            if(p1 == -1){
                return p2Next + 1;
            }
            if(p2Next == -1){
                return p1;
            }
            return Math.min(p1,1 + p2Next);
        }
    }
    
    //记忆化搜索
    public static int getAim2(int[] arr,int aim){
        int[][] dp = new int[arr.length+1][aim+1];
        //初始化这张表
        for(int i = 0;i < dp.length;i++){
            for(int j = 0;j < dp[i].length;j++){
                dp[i][j] = -2;
            }
        }
        return process2(arr,0,aim,dp);
    }
    
    public static int process2(int[] arr,int index,int rest,int[][] dp){
        if(rest < 0){  //相当于中了无效的缓存,缓存没办法表示,放在最前即可
            return -1;
        }
        if(dp[index][rest] != -2){
            return dp[index][rest];
        }
        if(rest == 0){
            dp[index][rest] = 0;
            return 0;
        }else if(index == arr.length){ //给定的硬币无法组成
            dp[index][rest] = -1;
            return -1;
        }else {    //rest > 0 并且还有 硬币
            int p1 = process2(arr, index + 1, rest,dp);
            int p2Next = process2(arr, index + 1, rest - arr[index],dp);
            if (p1 == -1 && p2Next == -1) {
                dp[index][rest] = -1;
            } else {
                if (p1 == -1) {
                    dp[index][rest] = 1 + p2Next;
                }
                if (p2Next == -1) {
                    dp[index][rest] = p1;
                }
                dp[index][rest] = Math.min(p1,1 + p2Next);
            }
        }
        return dp[index][rest];
    }
    
    //动态规划
    public static int getAim3(int[] arr,int aim){
        int[][] dp = new int[arr.length+1][aim+1];
        
        for(int index = 0;index <= arr.length;index++){  //第一列都为0
            dp[index][0] = 0;
        }
        for(int rest = 0;rest <= aim;rest++){  //最后一行都为0
            dp[arr.length][rest] = -1;
        }
        
        for(int index = arr.length - 1;index >= 0;index--){
            for(int rest = 1;rest <= aim;rest++){

                int p1 = dp[index + 1][rest];
                int p2Next = -1;
                if(rest - arr[index] >= 0){
                    p2Next = dp[index + 1][rest - arr[index]];
                }
                
                if(p1 == -1 && p2Next == -1){
                    dp[index][rest] = -1;
                }else {
                    if(p1 == -1){
                        dp[index][rest] = 1 + p2Next;
                    }
                    if(p2Next == -1){
                        dp[index][rest] = p1;
                    }
                    dp[index][rest] = Math.min(p1,1 + p2Next);
                }
            }
        }
        return dp[0][aim];
    }
}

问题3:马走棋盘

/**
 * 一只马在棋盘上走K步到end点,有多少种方法
 */
public class HouseJump {

    public static int getWays(){
       return process(0,0,10);
    }

    //默认马在 (0,0)位置
    //要去往(x,y)位置,必须跳 step 步
    //返回方法数
    public static int process(int x,int y,int step){
        if(x < 0 || x > 8 || y < 0 || y > 9){
            return 0;
        }
        if(step == 0){
            return (x == 0 && y == 0) ? 1 : 0;
        }
        //没越界,并且可以跳
        return process(x - 2,y + 1,step - 1) +
                process(x - 2,y - 1,step - 1) +
                process(x - 1,y + 2,step - 1) +
                process(x - 1,y - 2,step - 1) +
                process(x + 1,y + 2,step - 1) +
                process(x + 1,y - 2,step - 1) +
                process(x + 2,y + 1,step - 1) +
                process(x + 2,y - 1,step - 1);
    }


    //动态规划
    public static int getWay2(int x,int y,int step){
        if(x < 0 || x > 8 || y < 0 || y > 9 || step < 0){
            return 0;
        }
        int[][][] dp = new int[9][10][step+1];
        dp[0][0][0] = 1;  //第0层的一个面只有(0,0)为1,其他都为0
        for(int h= 1;h <= step;h++){
            for(int x1 = 0;x1 < 9;x1++){
                for (int y1 = 0;y1 < 10;y1++){
                    dp[x1][y1][h] += getValue(dp,x1 - 1,y1 + 2,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 - 1,y1 - 2,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 - 2,y1 + 1,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 - 2,y1 + 1,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 + 1,y1 + 2,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 + 1,y1 - 2,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 + 2,y1 + 1,h - 1);
                    dp[x1][y1][h] += getValue(dp,x1 - 2,y1 - 1,h - 1);
                }
            }
        }
        return dp[x][y][step];
    }

    //防止越界,越界就是0
    public static int getValue(int[][][] dp,int row,int col,int step){
        if(row < 0 || row > 8 || col < 0 || col > 9){
            return 0;
        }
        return dp[row][col][step];
    }
}

问题4:人存活概率

/**
 * 人在给定一个范围内走,只可以上下左右
 */
public class PeopleLive {

    // N M:表示范围的行和列  a b:人现在所在的位置  K:要走多少步
    public static String live(int N,int M,int a,int b,int K){
        long live = process(N,M,a,b,K);
        long all = (long)Math.pow(4,K);
        long gcd = gcd(all,live);
        return String.valueOf((live / gcd) + "/" + (all / gcd));
    }

    //获得a和b的最大公约数
    public static long gcd(long a,long b){
        return b == 0 ? a : gcd(a,a % b);
    }

    //N,M 区域(0~N-1  0~M-1),固定参数  x,y 当前位置  rest 还有多少步
    public static long process(int N,int M,int x,int y,int rest){
        if(x < 0 || x == N || y < 0 || y == M){
            return 0;
        }
        //没越界
        if (rest == 0){
            return 1;
        }
        //还没走完并且没有越界
        return process(N,M,x - 1,y,rest - 1) +
                process(N,M,x + 1,y,rest - 1) +
                process(N,M,x,y - 1,rest - 1) +
                process(N,M,x,y + 1,rest - 1);
    }

    //动态规划
    public static int live2(int N,int M,int x,int y,int step){
        if(x < 0 || x == N || y < 0 || y == M || step < 0){
            return 0;
        }
        int[][][] dp = new int[N][M][step+1];
        for(int i = 0;i < N;i++){  //第0层的一个面都为1
            for(int j = 0;j < M;j++){
                dp[i][j][0] = 1;
            }
        }
        for(int h= 1;h <= step;h++){
            for(int x1 = 0;x1 < N;x1++){
                for (int y1 = 0;y1 < M;y1++){
                    dp[x1][y1][h] += getValue(N,M,dp,x1 - 1,y1,h - 1);
                    dp[x1][y1][h] += getValue(N,M,dp,x1 + 1,y1,h - 1);
                    dp[x1][y1][h] += getValue(N,M,dp,x1,y1 + 1,h - 1);
                    dp[x1][y1][h] += getValue(N,M,dp,x1,y1 - 1,h - 1);
                }
            }
        }
        return dp[x][y][step];
    }

    public static int getValue(int N,int M,int[][][] dp,int row,int col,int step){
        if(row < 0 || row > N-1 || col < 0 || col > M-1){
            return 0;
        }
        return dp[row][col][step];
    }

}

问题4:零钱组成数

动态规划中出现枚举行为,观察周围与自己是否有联系。

public class Coin2 {

    //arr里都是正数,没有重复值,每一个值代表一种货币,每一种都可以用无线张
    //最终要找零钱数 aim
    //找零方法数返回
    public static int way1(int[] arr,int aim){
       return process(arr,0,aim);
    }

    //
    public static int process(int[] arr,int index,int rest){
        if(index == arr.length){
            return rest == 0 ? 1 : 0;
        }
        //arr[index]  可以使用0张,一张,。。。不超过rest即可
        int ways = 0;
        for(int zhang = 0;arr[index] * zhang <= rest;zhang++){
            ways += process(arr,index + 1,rest - arr[index] * zhang);
        }
        return ways;
    }

    //动态规划
    public static int way2(int[] arr,int aim){
        if(arr == null || arr.length == 0){
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N+1][aim+1];
        dp[N][0] = 1;  //初始时只有N行0列为1

        for(int index = N-1;index >= 0;index--){  //从下网上计算的
            for(int rest = 0;rest <= aim;rest++){
                int ways = 0;
                for(int zhang = 0;arr[index] * zhang <= rest;zhang++){
                    ways += dp[index + 1][rest - arr[index] * zhang];
                }
                dp[index][ways] = ways;
            }
        }
        return dp[0][aim];
    }

    public static int way3(int[] arr,int aim){
        if(arr == null || arr.length == 0){
            return 0;
        }
        int N = arr.length;
        int[][] dp = new int[N+1][aim+1];
        dp[N][0] = 1;  //初始时只有N行0列为1

        for(int index = N-1;index >= 0;index--){  //从下网上计算的
            for(int rest = 0;rest <= aim;rest++){  //对枚举行为进行优化
                dp[index][rest] = dp[index+1][rest];
                if(rest - arr[index] >= 0){
                    dp[index][rest] += dp[index][rest - arr[index]];
                }
            }
        }
        return dp[0][aim];
    }
}

14 有序表

删除节点、添加节点

红黑树   AVL   SB   跳表                 时间复杂度O(logN)

红黑树、AVL树、SB树都属于搜索二叉树系列

AVL

二叉树的左旋和右旋(头节点倒向哪边,就是什么旋)

  在二叉树在保证搜索二叉树的前提下,二叉树的左子树或者右子树可能会过长,就无法保证该二叉树是一颗平衡二叉树,因此对过长的那一颗子树进行左旋或者右旋。

LL型,进行右旋;RR型,进行左旋。对于LR型,先左旋再右旋;RL型,先右旋再左旋。

/**
 * 有序表之平衡二叉树
 */
public class Map_AVLTree {

    //平衡二叉树节点属性
    static class AVLNode<K extends Comparable<K>,V>{
        //封装平衡二叉树的节点信息
        public K key;
        public V value;
        //二叉树的左右孩子
        public AVLNode<K,V> left;
        public AVLNode<K,V> right;

        public int height;  //树的高度

        public AVLNode(K key,V value){
            this.key = key;
            this.value = value;
            height = 1;
        }
    }

    //有序表之平衡二叉树
    public static class AVLTreeMap<K extends Comparable<K>,V>{
        public AVLNode<K,V> root;  //整个有序表的根节点
        public int size;  //已经加入的key的个数

        public AVLTreeMap(){
            root = null;
            size = 0;
        }

        //获得node树的高度
        private int height(AVLNode<K,V> node) {
            return node == null ? 0 : node.height;
        }

        //更新node树的高度
        private void updateNodeHeight(AVLNode<K,V> node){
            if (node != null) node.height = Math.max(height(node.left),height(node.right)) + 1;
        }

        //对cur节点的整个树进行左旋
        // 返回的是当前节点的右节点
        // 当前cur节点的右节点 为 cur右节点的左节点
        // 当前cur节点 为 之前cur右节点的左节点
        private AVLNode<K,V> leftRotate(AVLNode<K,V> cur){ //左旋转后,当前节点cur 和 cur.right的高度都发生变化
            AVLNode<K,V> temp = cur.right;
            cur.right = temp.left;
            temp.left = cur;
            //左旋过后,cur变成子树,一定要先调整子树的高度
            updateNodeHeight(cur);
            updateNodeHeight(temp);
            return temp;
        }

        //对cur节点的整棵树进行右旋
        // 返回的是当前节点的左节点
        // 当前节点cur的左节点  为 cur左节点的右节点
        // 当前cur 节点 为 之前cur左节点的右节点
        private AVLNode<K,V> rightRotate(AVLNode<K,V> cur){
            AVLNode<K,V> temp = cur.left;
            cur.left = temp.right;
            temp.right = cur;
            //右旋过后,cur变为子树,先调整子树的高度
            updateNodeHeight(cur);
            updateNodeHeight(temp);
            return temp;
        }

        //平衡这颗二叉树,并返回平衡后的二叉树的根节点
        private AVLNode<K,V> rebalance(AVLNode<K,V> node){
            if(node == null){
                return null;
            }
            int leftHeight = height(node.left);
            int rightHeight = height(node.right);
            int nodeBalance = rightHeight - leftHeight;
            if(nodeBalance > 1){  // R
                if(node.right.right != null){  // R    RR 左移一次即可
                    node = leftRotate(node);
                }else {  // L   RL 先对右节点右移,再对整棵树左移
                    node.right = rightRotate(node.right);
                    node = leftRotate(node);
                }
            }else if(nodeBalance < -1){  // L
                if(node.left.left != null){ // L   LL 右移一次即可
                    node = rightRotate(node);
                }else {
                    node.left = leftRotate(node.left);
                    node = rightRotate(node);
                }
            }
            return node;
        }

        //添加节点
        // 在以cur为头的整颗子树上,加记录,并且把整棵树的头结点返回
        public AVLNode<K,V> add(AVLNode<K,V> cur,K key,V value){
            if(cur == null){
                return new AVLNode(key,value);
            }else {
                if(key.compareTo(cur.key) < 0){
                    cur.left = add(cur.left,key,value);
                }else {
                    cur.right = add(cur.right,key,value);
                }
            }
            updateNodeHeight(cur);//更新cur节点树的高度
            //每次添加,都需要进行一次二叉树的平衡
            return rebalance(cur);
        }

        // 在cur这棵树上,删掉key所代表的节点
        // 返回cur这棵树的新头部
        public AVLNode<K,V> delete(AVLNode<K,V> cur,K key){
            if(key.compareTo(cur.key) > 0){
                cur.right = delete(cur.right,key);
            }else if(key.compareTo(cur.key) < 0){
                cur.left = delete(cur.left,key);
            }else {
                if(cur.left == null && cur.right == null){
                    cur = null;
                }else if(cur.left == null && cur.right != null) {
                    cur = cur.right;
                }else if(cur.left != null && cur.right == null){
                    cur = cur.left;
                }else {  //不是叶子节点,用右树上的最左节点代替
                    // 找到右树上的最左结点
                    AVLNode<K,V> des=cur.right;
                    while (des.left!=null){
                        des=des.left;
                    }
                    // 先在右树调一个delete()方法删掉最左结点,完成右树的平衡调整,
                    // 然后得到最左结点,替换要删除的结点,然后依次往上检查平衡性
                    cur.right=delete(cur.right,des.key);
                    des.left=cur.left;
                    des.right=cur.right;
                    cur=des;
                }
            }
            updateNodeHeight(cur);
            // cur会从要删除的节点处回退到根节点,每次要保证子树是平衡的
            return rebalance(cur);
        }
    }
}

SB

平衡性:

每颗子树的大小,不小于其兄弟的子树大小;即每颗叔叔树的大小,不小于其任何子树的大小

 [B]  max{[G],[H]}  [C]  max{[E],[F]}

SB树的四种违规:目前所在的节点为节点A

    LL违规:A节点的左孩子B的左孩子E的节点个数 > A节点的右孩子C的节点个数

    LR违规:A节点的左孩子B的右孩子F的节点个数 > A节点的右孩子C的节点个数

    RL违规:A节点的右孩子C的左孩子G的节点个数 > A节点的左孩子B的节点个数

    RR违规:A节点的右孩子C的右孩子H的节点个数 > A节点的左孩子B的节点个数

不管是属于四种违规类型的哪种,调整方式跟AVL树一样,都是左旋或者右旋;唯一的区别就是旋转完后,哪个结点的孩子发生了变化,就要调到用平衡调整

public class Map_SBTree {
    public static class SBTNode<K extends Comparable<K>, V> {
        public K key;
        public V value;
        public SBTNode<K, V> left;
        public SBTNode<K, V> right;
        public int size; // 不同的key的数量

        public SBTNode(K key, V value) {
            this.key = key;
            this.value = value;
            size = 1;
        }
    }

    public static class SizeBalancedTreeMap<K extends Comparable<K>, V> {
        private SBTNode<K, V> root;

        //右旋转
        private SBTNode<K, V> rightRotate(SBTNode<K, V> cur) {
            SBTNode<K, V> leftNode = cur.left;
            cur.left = leftNode.right;
            leftNode.right = cur;
            leftNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            return leftNode;
        }

        //左旋转
        private SBTNode<K, V> leftRotate(SBTNode<K, V> cur) {
            SBTNode<K, V> rightNode = cur.right;
            cur.right = rightNode.left;
            rightNode.left = cur;
            rightNode.size = cur.size;
            cur.size = (cur.left != null ? cur.left.size : 0) + (cur.right != null ? cur.right.size : 0) + 1;
            return rightNode;
        }

        private SBTNode<K, V> maintain(SBTNode<K, V> cur) {
            if (cur == null) {
                return null;
            }
            int leftSize = cur.left != null ? cur.left.size : 0;
            int leftLeftSize = cur.left != null && cur.left.left != null ? cur.left.left.size : 0;
            int leftRightSize = cur.left != null && cur.left.right != null ? cur.left.right.size : 0;
            int rightSize = cur.right != null ? cur.right.size : 0;
            int rightLeftSize = cur.right != null && cur.right.left != null ? cur.right.left.size : 0;
            int rightRightSize = cur.right != null && cur.right.right != null ? cur.right.right.size : 0;
            if (leftLeftSize > rightSize) {  //LL
                cur = rightRotate(cur);
                //旋转过后,那个节点上的子节点变化了,就需要进行平衡调整
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (leftRightSize > rightSize) {  //LR
                cur.left = leftRotate(cur.left);
                cur = rightRotate(cur);
                //旋转过后,返回的新的头节点以及头节点的左右孩子都发生变化
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            } else if (rightRightSize > leftSize) {  //RR
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur = maintain(cur);
            } else if (rightLeftSize > leftSize) {  //RL
                cur.right = rightRotate(cur.right);
                cur = leftRotate(cur);
                cur.left = maintain(cur.left);
                cur.right = maintain(cur.right);
                cur = maintain(cur);
            }
            return cur;
        }

        // 现在,以cur为头的树上,新增,加(key, value)这样的记录
        // 加完之后,会对cur做检查,该调整调整
        // 返回,调整完之后,整棵树的新头部
        private SBTNode<K, V> add(SBTNode<K, V> cur, K key, V value) {
            if (cur == null) {
                return new SBTNode<>(key, value);
            } else {
                cur.size++;
                if (key.compareTo(cur.key) < 0) {
                    cur.left = add(cur.left, key, value);
                } else {
                    cur.right = add(cur.right, key, value);
                }
                return maintain(cur);
            }
        }

        // 在cur这棵树上,删掉key所代表的节点
        // 返回cur这棵树的新头部
        public SBTNode<K, V> delete(SBTNode<K, V> cur, K key) {
            cur.size--;
            if (key.compareTo(cur.key) < 0) {
                cur.left = delete(cur.left, key);
            } else if (key.compareTo(cur.key) > 0) {
                cur.right = delete(cur.right, key);
            } else {// 当前要删掉cur
                if (cur.left == null && cur.right == null) {
                    // free cur memory -> C++
                    cur = null;
                } else if (cur.left == null && cur.right != null) {
                    // free cur memory -> C++
                    cur = cur.right;
                } else if (cur.left != null && cur.right == null) {
                    // free cur memory -> C++
                    cur = cur.left;
                } else {// 左右孩子都有
                    // 找到cur的后继结点替换cur
                    SBTNode<K, V> pre = null;
                    SBTNode<K, V> des = cur.right;// des来到当前结点的右孩子
                    des.size--;
                    while (des.left != null) {
                        pre = des;
                        des = des.left;
                        des.size--;
                    }
                    // while循环结束后,des来到了cur结点的右树的最左孩子
                    // 并且此时的des是叶子结点,没有孩子了
                    // pre来到最左孩子的父亲结点
                    if (pre != null) {
                        pre.left = des.right;// 最左孩子的父亲断掉最左孩子
                        des.right = cur.right;// 最左孩子接管cur的右子树
                    }
                    des.left = cur.left;// 还是接管原来的左子树
                    des.size = des.left.size + (des.right == null ? 0 : des.right.size) + 1;
                    cur = des;
                }
            }
            // return maintain(cur);
            return cur;
        }
    }
       
}

跳表

 最左边的是header节点,不存值,上图的31,出现在了0,1,2,3层,其实就是一个节点。不是四个节点(这个要看具体的实现,这里是通过数组实现,可以通过下标访问,也可以通过链式实现)。这些层次信息是通过forwards(ArrayList)保存的。因此可以很快的访问到下一层。

每次插入新的数据时,如果这个数据没有,会随机生成一个等级level,表示该数据的层数,然后再插入到跳表中。

查询时,从头header的最高等级开始查询,每次查到 < key的最大一个数,然后查到对应数字,但没有查到0层,因此要以当前查到层数继续向下查 < key的最大一个数

eg:查询16,先从header最高层出发,发现31>16,不行,向下到第二层,查询到2<16,继续以第二场相后查询,发现31>16,不行,则再该数据向下层查询,在2的数据中查询第一层,查到15<16,则跳到15,继续以第一层向后查询,发现31>16,不行,则在15这个数向下一层查询,查询0层,发现16=16,满足。

import java.util.ArrayList;

public class Map_SkipList {

    // 跳表的结点定义
    public static class SkipListNode<K extends Comparable<K>,V>{
        public K key;
        public V val;
        public ArrayList<SkipListNode<K,V>> nextNodes;

        public SkipListNode(K k,V v){
            key=k;
            val=v;
            nextNodes=new ArrayList<SkipListNode<K,V>>();
        }

        // 遍历的时候,如果是往右遍历到的null(next == null), 遍历结束
        // 头(null), 头节点的null,认为最小
        // node  -> 头,node(null, "")  node.isKeyLess(!null)  true
        // node里面的key是否比otherKey小,true,不是false
        public boolean isKeyLess(K otherKey){
            // otherKey==null -> false
            return otherKey!=null && (key==null || key.compareTo(otherKey)<0);
        }

        public boolean isKeyEqual(K otherKey){
            return (key==null && otherKey==null) ||
                    (key!=null && otherKey!=null && key.compareTo(otherKey)==0);
        }
    }

    public static class SkipListMap<K extends Comparable<K>,V> {
        private static final double PROBABILITY = 0.5;// <0.5继续做,>=0.5就停
        private SkipListNode<K, V> head;
        private int size;
        private int maxLevel;

        public SkipListMap() {
            head = new SkipListNode<>(null, null);
            head.nextNodes.add(null);
            size = 0;
            maxLevel = 0;
        }

        // 从最高层开始,一路找下去,
        // 最终,找到第0层的 <key的最右的节点
        private SkipListNode<K, V> mostRightLessNodeInTree(K key) {
            if (key == null) {
                return null;
            }
            int level = maxLevel;
            SkipListNode<K, V> cur = head;
            // 从上层跳下层
            while (level >= 0) {
                cur = mostRightLessNodeInLevel(key, cur, level--);
            }
            return cur;
        }

        // 在level层里,如何往右移动
        // 现在来到的节点是cur,来到了cur的level层,在level层上,找到 <key最后一个节点并返回
        private SkipListNode<K, V> mostRightLessNodeInLevel(K key, SkipListNode<K, V> cur, int level) {
            // 上面层跳过一个,下面层就会跳过一批,优势就体现在这里
            SkipListNode<K, V> next = cur.nextNodes.get(level);
            while (next != null && next.isKeyLess(key)) {
                cur = next;
                next = cur.nextNodes.get(level);
            }
            return cur;
        }

        public boolean containsKey(K key) {
            if (key == null) {
                return false;
            }
            SkipListNode<K, V> less = mostRightLessNodeInTree(key);
            SkipListNode<K, V> next = less.nextNodes.get(0);
            return next != null && next.isKeyEqual(key);
        }

        // 新增,修改value
        public void put(K key, V value) {
            if (key == null) {
                return;
            }
            // 0层上,最右一个,< key 的Node -> >key
            SkipListNode<K, V> less = mostRightLessNodeInTree(key);
            SkipListNode<K, V> find = less.nextNodes.get(0);
            if (find != null && find.isKeyEqual(key)) {// 直接更新
                find.val = value;
            } else {// find==null
                size++;
                int newNodeLevel = 0;
                while (Math.random() < PROBABILITY) {
                    newNodeLevel++;
                }
                // newNodeLevel
                while (newNodeLevel > maxLevel) {
                    head.nextNodes.add(null);
                    maxLevel++;
                }
                SkipListNode<K, V> newNode = new SkipListNode<>(key, value);
                for (int i = 0; i <= newNodeLevel; i++) {
                    newNode.nextNodes.add(null);
                }
                int level = maxLevel;
                SkipListNode<K, V> pre = head;
                while (level >= 0) {
                    // level 层中,找到最右的 < key 的节点
                    pre = mostRightLessNodeInLevel(key, pre, level);
                    if (level <= newNodeLevel) {
                        newNode.nextNodes.set(level, pre.nextNodes.get(level));
                        pre.nextNodes.set(level, newNode);
                    }
                    level--;
                }
            }
        }

        public void remove(K key) {
            if (containsKey(key)) {
                size--;
                int level = maxLevel;
                SkipListNode<K, V> pre = head;
                while (level >= 0) {
                    pre = mostRightLessNodeInLevel(key, pre, level);
                    SkipListNode<K, V> next = pre.nextNodes.get(level);
                    // 1)在这一层中,pre下一个就是key
                    // 2)在这一层中,pre的下一个key是>要删除key
                    if (next != null && next.isKeyEqual(key)) {
                        // free delete node memory -> C++
                        // level : pre -> next(key) -> ...
                        // 前一个结点在level层的指针指向要删除的下一个结点
                        pre.nextNodes.set(level, next.nextNodes.get(level));
                    }
                    // 在level层只有一个节点了,就是默认节点head
                    if (level != 0 && pre == head && pre.nextNodes.get(level) == null) {
                        head.nextNodes.remove(level);
                        maxLevel--;
                    }
                    level--;
                }
            }
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
武汉理工大学的数据结构与算法实验包括线性表的应用、栈的应用、的应用、无向图的应用、有向图的应用、查找与排序、哈夫曼编码等内容。这些实验旨在帮助学生掌握数据结构的设计和应用,包括图的存储结构和创建方法、图的遍历方法、迪杰斯特拉算法、最小生成的概念、普里姆算法等。此外,还有一个名为"简易计算器程序"的实验项目,项目内容是编写程序来模拟简单运算器的工作,实现对中缀表达式的计算。希望通过这些实验,学生能够加深对数据结构和算法的理解,并提升编程能力。123 #### 引用[.reference_title] - *1* *3* [武汉理工大学数据结构课内实验](https://blog.csdn.net/mo_zhe/article/details/112733427)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] - *2* [武汉理工大学数据结构综合实验——图与景区信息管理系统实践](https://blog.csdn.net/mo_zhe/article/details/112771599)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值