剑指offer:题解(61-67)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


61.序列化二叉树

描述见链接

1.解法一:序列化采用前序遍历

public class Solution {
    String Serialize(TreeNode root) {
        String res = "";
        return Serialize(root, res);
    }
    private String Serialize(TreeNode root, String res) {
        if (root == null) {	// 序列化根节点
            res += "#!";
            return res;
        } else {
            res += root.val + "!";
        }
        res = Serialize(root.left, res);	// 序列化左子树
        res = Serialize(root.right, res);	// 序列化右子树
        return res;
    }

    TreeNode Deserialize(String str) {
        if (str == null || str.length() == 0) 
            return null;
        // 现将序列根据规则拆分成字符串数组
        String[] arr = str.split("!");
        return Deserialize(arr);
    }
    int start = -1;
    private TreeNode Deserialize(String[] str) {
        start ++;
        // 当前字符串为空,说明是一个空节点,不用考虑
        if (start < str.length && !str[start].equals("#")) {
            // 将字符串数组的值转换为整数
            TreeNode curr = new TreeNode(Integer.parseInt(str[start]));
            curr.left = Deserialize(str);	// 因为每一次递归 start 值都会增加
            curr.right = Deserialize(str);
            return curr;
        }
        return null;
    }
}

62.二叉搜索树的第k个节点

题目描述: 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

1.解法一:利用二叉搜索树的中序遍历是有序的,使用额外空间

public class Solution {
    private ArrayList<TreeNode> list = new ArrayList<>();
    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot == null) return null;
        inOrder(pRoot);
        if (k < 1 || k > list.size()) return null;
        return list.get(k - 1);
    }
    // 递归中序遍历
    private void inOrder(TreeNode root) {
        if (root != null) {
            inOrder(root.left);
            list.add(root);
            inOrder(root.right);
        }
    }
}

2.解法二:不使用额外空间,使用一个计数器记录遍历的元素个数

public class Solution {
    private int count = 0;    // 使用count来记录中序访问了多少节点
    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot != null) {
            TreeNode node = KthNode(pRoot.left, k);	// 先去左边找
            if (node != null)    return node;
            
            count ++;	// 没找到,看看中间是不是
            if (count == k)    return pRoot;
            
            node = KthNode(pRoot.right, k);	// 再去右边找
            if (node != null)    return node;
        }
        return null;
    }
}

63.数据流中的中位数

题目描述: 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

1.解法一:使用最大最小堆

插入的时间复杂度为O(log n) ,查找的时间复杂度O(1)

当数据保存到容器中时,可以分为两个部分,左边一部分的数据要比右边一部分的数据小。如下图所示P1是左边最大的数,P2是右边最小的数,即使左右两部分数据不是有序的,我们也可以知道:左边的最大数小于右边最小的数

由此,我们可以得到一个思路:用一个最大堆实现左边的数据存储,一个最小堆实现右边的数据存储, 向堆中插入一个数据的时间是O(log n),而中位数就是堆顶的数据,只需要O(1) 的时间就可以得到。

在具体的操作中:为确保中位数出现在堆顶,两个堆的数据量之差不超过1。为了实现平均分配,我们可以设定在来到的数据量是偶数时,将数据插入最小堆,否则插入最大堆。

另外,还要确保最大堆中的所有元素小于最小堆中的元素。 所以,新传来的数据要和最大堆中的最大值或最小堆中的最小值进行比较

  1. 数据量为偶数插入最小堆,但是如果新来的数据小于最大堆的最大值,要先将新数据插入最大堆,将最大堆的堆顶元素弹出加入最小堆,这样才能保证最大堆的最大值小于最小堆的最小值。
  2. 数据量为奇数插入最大堆,同理,要是大于最小堆的最小值,那么就将其插入最小堆,然后将最小堆的堆顶元素弹出加入最大堆。

如果用其他数据结构进行存储,时间复杂度总结如下:(来源

public class Solution {
    // 优先队列默认最小堆,使用比较器构建最大堆
    private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    // 使用lambda表达式,上面的最大堆可以写成
    // private PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
    private int count = 0;
    public void Insert(Integer num) {
        count ++;
        if (count % 2 == 0) {	// 偶数,插入最小堆
            if (!maxHeap.isEmpty() && num < maxHeap.peek()) {	// 如果num<最大堆,那么先插入最大堆
                maxHeap.add(num);
                num = maxHeap.poll();
            }
            minHeap.add(num);
        } else {	// 奇数,插入最大堆
            if (!minHeap.isEmpty() && num > minHeap.peek()) {
                minHeap.add(num);
                num = minHeap.poll();
            }
            maxHeap.add(num);
        }
    }

    public Double GetMedian() {
        if (minHeap.size() == maxHeap.size()) {
            return (minHeap.peek() + maxHeap.peek()) / 2.0;
        } else if (minHeap.size() < maxHeap.size()) {
            return maxHeap.peek() / 1.0;
        } else {
            return minHeap.peek() / 1.0;
        }
    }
}

64.滑动窗口的最大值

描述见链接

1.解法一

public class Solution {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> res = new ArrayList<>();
        if (num == null || num.length < 1 || size <= 0 || size > num.length)
            return res;
        // 使用一个双端队列
        Deque<Integer> q = new LinkedList<>();    // 存放的是数组的下标
        for (int i = 0;i < num.length;i ++) {
            // 队列不为空且队列的长度超过了规定的size
            while (!q.isEmpty() && q.peek() < i - size + 1) {
                q.poll();
            }
            // 队列不为空且插入的元素比队尾元素大,那么就删除队尾,插入这个元素的索引
            while (!q.isEmpty() && num[i] >= num[q.getLast()]) {
                q.removeLast();
            }
            q.add(i);
            // 只有当下标i超过size - 1,才说明开始有了滑动窗口
            if (i >= size - 1) {
                // q存放的是索引
                res.add(num[q.peek()]);
            }
        }
        return res;
    }
}

65.矩阵中的路径

描述见链接

1.解法一:回溯法

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
        if (matrix == null || matrix.length == 0 || str == null || str.length == 0)
            return false;
        // 首先把一维的matrix转换为二维的
        char[][] mat = new char[rows][cols];
        boolean[][] flag = new boolean[rows][cols];
        for (int i = 0;i < rows;i ++) {
            for (int j = 0;j < cols;j ++) {
                mat[i][j] = matrix[i * cols + j];
            }
        }
        boolean res = false;
        for (int i = 0;i < rows;i ++) {
            for (int j = 0;j < cols;j ++) {
                res = hasOnePath(mat, i, j, rows, cols, flag, str, 0);
                // 不能直接返回false,因为这个位置找不到,可能在下一个位置就找到了
                if (res == true)
                    return true;
            }
        }
        return false;
    }
    private boolean hasOnePath(char[][] mat, int i, int j, int rows, int cols, boolean[][] flag, char[] str, int start) {
        // start代表已经找到str中第几个元素了
        if (start == str.length)
            return true;
        if (i < 0 || i >= rows || j < 0 || j >= cols) 
            return false;
        // 下面说明还在遍历之中
        boolean res = false;
        if (mat[i][j] == str[start] && flag[i][j] == false) {
            flag[i][j] = true;
            start ++;
            // 分别取上下左右四个位置去寻找
            res  = hasOnePath(mat, i + 1, j, rows, cols, flag, str, start)
                || hasOnePath(mat, i - 1, j, rows, cols, flag, str, start)
                || hasOnePath(mat, i, j - 1, rows, cols, flag, str, start)
                || hasOnePath(mat, i, j + 1, rows, cols, flag, str, start);
            // 如果没有找到,就可以直接回溯了,而不是直接返回值
            if (res == false) {
                start --;
                flag[i][j] = false;
            }
        }
        return res;       
    }
}

2.解法二:其实就是解法一换了个写法

public class Solution {
    private char[][] mat;    // 设置成全局变量,减少函数中的参数
    private boolean[][] flag;    // 记录这个位置是否被使用过,默认都是false
    private int rows, cols;
    private char[] str;
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
        if (matrix == null || str == null || rows < 1 || cols < 1) return false;
        mat = new char[rows][cols];
        flag = new boolean[rows][cols];
        this.rows = rows;
        this.cols = cols;
        this.str = str;
        //将一维数组转化为二维的
        for (int i = 0;i < rows;i ++) {
            for (int j = 0;j < cols;j ++) {
                mat[i][j] = matrix[i * cols + j];
            }
        }
        boolean res = false;
        for (int i = 0;i < rows;i ++) {
            for (int j = 0;j < cols;j ++) {
                res = hasOnePath(i, j, 0);
                if (res == true)
                    return true;
            }
        }
        return false;
    }
    private boolean hasOnePath(int i, int j, int start) {
        if (start == str.length)
            return true;
        if (i < 0 || i >= rows || j < 0 || j >= cols)
            return false;
        boolean res = false;
        if (mat[i][j] == str[start] && flag[i][j] == false) {
            flag[i][j] = true;
            start ++;
            res  = hasOnePath(i + 1, j, start)
                || hasOnePath(i - 1, j, start)
                || hasOnePath(i, j + 1, start)
                || hasOnePath(i, j - 1, start);
            if (res == false) {
                flag[i][j] = false;
                start --;
            }
        }
        return res;
    }
}

66.机器人的运动范围

题目描述: 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子?

1.解法一:回溯法

public class Solution {
    private int threshold, rows, cols;
    public int movingCount(int threshold, int rows, int cols) {
        if (rows < 1 || cols < 1 || threshold < 0) return 0;
        boolean[][] flag = new boolean[rows][cols];
        this.threshold = threshold;
        this.rows = rows;
        this.cols = cols;
        return moveCount(0, 0, flag);
    }
    // 如果当前位置可达,那么将结果加一,并开始向上下左右寻找,走过的路就标为true,不用返回false
    // 因为false都是不可达的
    private int moveCount(int i, int j, boolean[][] flag) {
        int count = 0;
        if (checkReachable(i, j, flag)) {
            flag[i][j] = true;
            count = 1 + moveCount(i + 1, j, flag)
                + moveCount(i - 1, j, flag)
                + moveCount(i, j + 1, flag)
                + moveCount(i, j - 1, flag);
        }
        return count;
    }
    // 检查当前位置是否可达
    private boolean checkReachable(int i, int j, boolean[][] flag) {
        // 索引越界
        if (i < 0 || i >= rows || j < 0 || j >= cols)
            return false;
        // 数位越阈值或当前位置已被访问过
        if (digitSum(i) + digitSum(j) > threshold || flag[i][j] == true)
            return false;
        return true;
    }
    // 获取当前索引的每一位的和
    private int digitSum(int num) {
        int sum = 0;
        while (num != 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}

67.剪绳子

描述见链接

1.解法一:动态规划

public class Solution {
    public int cutRope(int target) {
        if (target == 2) return 1;
        if (target == 3) return 2;
        int[] dp = new int[target + 1];
        // 下面三个情况是在 target >= 4 时产生的
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        int res = 0;
        for (int i = 4;i <= target;i ++) {
            for (int j = 1;j <= i / 2;j ++) {
                res = Math.max(res, dp[j] * dp[i - j]);
            }
            dp[i] = res;
        }
        return dp[target];
    }
}

2.解法二:使用贪心算法

target > 3时,将target尽可能的分为 3 的和将使乘积最大化。将target分为 3 的和,可以有三种情况:

  1. target % 3 == 0,此时最大值为Math.pow(3, target / 3)
  2. target % 3 == 1,此时多出来的那个 1 可以加在一个 3 上面,所以最大值为4 * Math.pow(3, target / 3 - 1)
  3. target % 3 == 2,此时少的那个 1 在一个 3 里面减,由于凑不够整数个3,所以使用乘以 2 来解决,所以最大值为2 * Math.pow(3, target / 3)
public class Solution {
    public int cutRope(int target) {
        if (target == 2) return 1;
        if (target == 3) return 2;
        if (target % 3 == 0) return (int)Math.pow(3, target / 3);
        if (target % 3 == 1) return 4 * (int)Math.pow(3, target / 3 - 1);
        return 2 * (int)Math.pow(3, target / 3);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值