牛客剑指offer:题解(21-30)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


21.栈的压入、弹出序列

题目描述: 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

1.解法一

借用一个辅助栈模拟出栈的过程。

牛客网解答思路:遍历压栈顺序,先将第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。

public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        if (pushA.length == 0 || popA.length == 0) return false;
        Stack<Integer> s = new Stack<>();
        int popIndex = 0;
        for (int i = 0;i < pushA.length;i ++) {
            s.push(pushA[i]);
            while (!s.isEmpty() && popA[popIndex] == s.peek()) {
                s.pop();
                popIndex ++;
            }
        }
        return s.isEmpty();
    }
}

22. 从上往下打印二叉树(层序遍历)

题目描述: 从上往下打印出二叉树的每个节点,同层节点从左至右打印。

1.解法一:迭代,使用队列解决

public class Solution {
    // 相当于就是一个层序遍历
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> res = new ArrayList<>();
        Queue<TreeNode> q = new LinkedList<>();
        if (root == null) return res;
        q.add(root);
        while (!q.isEmpty()) {
            // 记录当前层的节点个数
            int size = q.size();
            for (int i = 0;i < size;i ++) {
                // 出队一个节点的同时将他的左右不为空孩子添加到队列中
                TreeNode node = q.remove();
                res.add(node.val);
                // 左右孩子要是为空,就不能往队列里面添加了
                if (node.left != null)    q.add(node.left);
                if (node.right != null)    q.add(node.right);               
            }
        }
        return res;
    }
}

23. 二叉搜索树的后序遍历序列

题目描述: 输入一个非空整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。

1.解法一:递归

BST 的后序序列的合法序列是,对于一个序列S,最后一个元素是 X(根节点值),如果去掉最后一个元素的序列为T,那么T满足:T可以分成两段,前一段(左子树)小于X,后一段(右子树)大于X,且这两段(子树)都是合法的后序序列。 这就是能使用递归的原因。

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if (sequence.length == 0) return false;
        return helper(sequence, 0, sequence.length - 1);
    }
    private boolean helper(int[] arr, int l, int r) {
        // 空树或是只有一个节点在已有原始根节点的情况下都是一个二叉搜索树
        if (l >= r) return true;  
        int i = r;
        // [l,i-1] 是左子树,[i, r - 1] 是右子树, arr[r] 是根节点
        while (i > l && arr[i - 1] > arr[r]) i --;
        // 确保左子树所有元素小于根节点元素
        for (int j = i - 1;j >= l;j --)
            if (arr[j] > arr[r]) return false;
        // 分别判断左右子树的合法性
        return helper(arr, l, i - 1) && helper(arr, i, r - 1);
    }
}

24. 二叉树中和为某一值的路径

题目描述: 输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

1.解法一

import java.util.ArrayList;
import java.util.Comparator;
public class Solution {
    private ArrayList<ArrayList<Integer>> res = new ArrayList<>();
    private LengthCompare c = new LengthCompare();    // 根据长度重排序res
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        if (root == null) return res;
        ArrayList<Integer> tmp = new ArrayList<>();
        backtrace(root, target, tmp);
        res.sort(c);	// 使用匿名内部类实现一个comparator也可以,更简洁
        return res;
    }
    private void backtrace(TreeNode root, int target, ArrayList<Integer> tmp) {
        tmp.add(root.val);
        if (root.left == null && root.right == null) { // 根节点情况
            if (root.val == target) {    // 找到一条路径
                res.add(new ArrayList(tmp));
            }
        } else { // 不是根节点
            if (root.left != null) 
                backtrace(root.left, target - root.val, tmp);
            if (root.right != null)
                backtrace(root.right, target - root.val, tmp);
        }
        // 回溯,要是不为空,回溯到上一个根节点
        if (!tmp.isEmpty())
            tmp.remove(tmp.size() - 1);
    }
    class LengthCompare implements Comparator<ArrayList> {
        // 因为长度长的要放到前面,所以返回值需要大的返回-1
        @Override
        public int compare(ArrayList a1, ArrayList a2) {
            if (a1.size() > a2.size()) return -1;
            else if (a1.size() < a2.size()) return 1;
            else return 0;
        }
    }
}

25. 复杂链表的复制

题目描述: 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

1.解法一:最优

先按next复制,但是把复制后的节点放到原节点后面,则可以很容易的添加 random,最后按照奇偶位置拆成两个链表,时间复杂度 O(n),不需要额外空间。

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        if (pHead == null) return pHead;
        //(1)先按next复制,但是把复制后的节点放到对应原节点后面
        copyNext(pHead);
        //(2)依次添加random指针
        addRandom(pHead);
        //(3)按照奇偶位置拆成两个链表
        return reconnect(pHead);
    }
    // 先复制next节点,并且把每一个next节点放到原节点的后面
    // A->A'->B->B'->C->C'
    private void copyNext(RandomListNode pHead) {
        RandomListNode head = pHead;
        while (head != null) {
            //复制一个结点,插在对应的原节点的后面
            RandomListNode tmp = new RandomListNode(head.label);
            tmp.next = head.next;
            tmp.random = null;
            head.next = tmp;
            head = tmp.next;
        }
    }
    // 再增加random指针
    private void addRandom(RandomListNode pHead) {
        RandomListNode head = pHead;
        while (head != null) {
            RandomListNode head_new = head.next;
            if (head.random != null) {
                head_new.random = head.random.next;
            }
            head = head_new.next;
        }
    }
    // 将奇偶位置分开
    private RandomListNode reconnect(RandomListNode pHead) {
        RandomListNode head = pHead;
        RandomListNode pHeadClone = head.next;
        RandomListNode headClone = pHeadClone;
        while (head != null) {
            head.next = headClone.next;
            head = head.next;
            // 当前 head 不为空,说明还有下一个节点,headClone可以继续往后连接
            if (head != null) {
                headClone.next = head.next;
                headClone = headClone.next;
            }
        }
        return pHeadClone;
    }
}

26. 二叉搜索树与双向链表

题目描述: 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

1.解法一:递归

  1. 首先,根据二叉搜索树的特点,左结点的值<根结点的值<右结点的值,据此不难发现,使用二叉树的中序遍历得到的数据序列就是递增的排序顺序。因此,首先确定应该采用中序遍历方法。
  2. 根据中序遍历的顺序,当我们遍历到根结点时,它的左子树已经转换为一个排好序的双向链表,并且链表最后一个结点是左子树值最大的结点,我们把这个值最大的结点同根结点链接起来,根节点就成了最后一个结点,更新一下最后一个节点,接着遍历右子树,将根结点同右子树中最小的结点链接起来。
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        //根据中序遍历采用递归依次实现
        if (pRootOfTree == null) return pRootOfTree;
        TreeNode curEndOfList = null;
        TreeNode root = pRootOfTree;
        Convert(root, curEndOfList);
        // 向左去寻找链表的头结点
        while (pRootOfTree != null && pRootOfTree.left != null) {
            pRootOfTree = pRootOfTree.left;
        }
        return pRootOfTree;
    }
    // curEndOfList 记录已经排好序的链表的末尾
    private TreeNode Convert(TreeNode pRootOfTree, TreeNode curEndOfList) {
        if (pRootOfTree == null) return pRootOfTree;
        TreeNode root = pRootOfTree;
        // 左子树不为空则将左子树构建成为双向链表并更新左子树双向链表的最后一个节点
        if (root.left != null) {
            curEndOfList = Convert(root.left, curEndOfList);
        }
        // 将根节点接在左子树的链表之后
        root.left = curEndOfList;
        // 如果链表末尾不为空,则将链表末尾连接上root
        if (curEndOfList != null) {
            curEndOfList.right = root;
        }
        // 连上根节点后,此时的最后一个节点就是根节点,
        // 所以更新链表末尾节点后,再去遍历根节点的右孩子节点
        curEndOfList = root;
        // 去遍历右孩子节点,并返回链表的最后一个节点
        if (root.right != null)
            curEndOfList = Convert(root.right, curEndOfList);
        return curEndOfList;
    }
}

27. 字符串的排列

1. 解法一:回溯法-其实就是全排列

public class Solution {
    private ArrayList<String> res = new ArrayList<>();
    private boolean[] used;
    public ArrayList<String> Permutation(String str) {
        if (str == null || str.length() == 0) return res;
        char[] arr = str.toCharArray();
        used = new boolean[arr.length];
        // 因为可能有重复元素,所以需要先排序
        Arrays.sort(arr);
        permute(arr, used, new StringBuilder());
        return res;
    }
    
    private void permute(char[] arr, boolean[] used, StringBuilder builder) {
        if (builder.length() == arr.length) {
            res.add(new String(builder.toString()));
        }
        for (int i = 0;i < arr.length;i ++) {
            // 去重
            if (used[i] || i != 0 && arr[i] == arr[i - 1] && !used[i - 1])
                continue;
            builder.append(arr[i]);
            used[i] = true;
            permute(arr, used, builder);
            used[i] = false;
            builder.deleteCharAt(builder.length() - 1);
        }
    }
}

28. 数组中出现次数超过一半的数字

题目描述: 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为 9 的数组{1,2,3,2,2,2,5,4,2}。由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2。如果不存在则输出 0。

1.解法一:使用额外空间记录每一个元素出现的次数

  1. 使用 HashMap 记录每一个元素出现的次数
  2. 遍历数组,如果这个元素出现的次数大于长度一半,则返回这个元素
  3. 时间复杂度 O(n)
  4. 空间复杂度 O(n)
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        HashMap<Integer,Integer> map = new HashMap<>();
        for (int i = 0;i < array.length;i ++) {
            if (!map.containsKey(array[i])) map.put(array[i], 1);
            else map.put(array[i], map.get(array[i]) + 1);
        }
        for (int i = 0;i < array.length;i ++) {
            if (map.get(array[i]) > array.length / 2) return array[i];
        }
        return 0;
    }
}

2.解法二:排序后,如果有最多数,那么一定位于中间

  1. 时间复杂度取决于排序算法 一般就是 O(n log n)
  2. 空间复杂度为 O(1)
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        // 如果多于数组一半,那么排序后,这个数一定出现在中位数位置
        Arrays.sort(array);
        // 获取中位数,如果中位数出现次数超过一半,就是中位数
        int half = array[array.length / 2];
        int count = 0;
        // 计算中位数出现的次数
        for (int i = 0;i < array.length;i ++) {
            if (array[i] == half) count ++;
        }
        return count > array.length / 2 ? half : 0;
    }
}

3.解法三:

  1. 根据数组特点得到时间复杂度为O(n) 的算法。
  2. 根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。
  3. 因此,我们可以在遍历数组的时候设置两个值:一个是数组中的数 result,另一个是出现次数 times。当遍历到下一个数字的时候,如果与 result 相同,则次数加 1,不同则次数减 1,当次数变为 0 的时候说明该数字不可能为多数元素,将r esult 设置为下一个数字,次数设为 1。
  4. 这样,当遍历结束后,最后一次设置的 result 的值可能就是符合要求的值(如果有数字出现次数超过一半,则必为该元素,否则不存在),因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回 0。
  5. 这种思路是对数组进行了两次遍历,复杂度为 O(n)。
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if (array == null || array.length == 0) return 0;
        int N = array.length;
        int res = array[0];	// 假定第一个元素就是最多数元素
        int times = 1;
        for (int i = 1;i < N;i ++) {
            if (array[i] == res)
                times ++;
            else if (times == 0) {
                // 更新res
                res = array[i];
                times = 1;
            } else 
                times --;
        }
        times = 0;
        // 判断res是否超过一半数
        for (int i = 0;i < N;i ++) {
            if (array[i] == res) times ++;
        }
        return times > N / 2 ? res : 0;
    }
}

29. 最小的 K 个数

题目描述: 输入 n 个整数,找出其中最小的 K 个数。例如输入 4,5,1,6,2,7,3,8 这 8 个数字,则最小的 4 个数字是 1,2,3,4。

1.解法一:先排序,再找前 K 个

  1. 时间复杂度 O(n log n)
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<>();
        if (input == null || input.length == 0 || k > input.length) return res;
        // 先排序,再找前K个
        Arrays.sort(input);
        for (int i = 0;i < k;i ++) {
            res.add(input[i]);
        }
        return res;
    }
}

2.解法二:使用 Partition 思想

  1. 。类似于快速排序的思想,基于 Partition 函数 来解决这个问题,如果我们选取数组的第 n 个数字(记为 key)来进行数组重排,那么比 key 小的所有数字都位于数组的左边,比 key 大的所有数字都位于 key 之后,也就是数组的右边。
  2. 因此,我们只需要判断 key 的下标是否等于 k-1,等于时返回其左边的 k 个数便是最小的 k 个数。当 key 的下标小于 k-1 时,就在右边继续划分,反之左边继续划分。由此我们可以得到以下代码实现。
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> res = new ArrayList<>();
        if (input == null || input.length == 0 || k > input.length || k == 0) return res;
        // p 左边的都比p小,右边的都比p大
        int p = partition(input, 0, input.length - 1);
        while (p != k - 1) {
            // 如果不是指定位置,那我们就更新P的值
            if (p < k - 1) 
                p = partition(input, p + 1, input.length - 1);
            else 
                p = partition(input, 0, p - 1);
        }
        for (int i = 0;i <= p;i ++) {
            res.add(input[i]);
        }
        return res;
    }
    private int partition(int[] arr, int left, int right) {
        // 选择最左边元素作为这个Key,
        // 如果数组接近有序,更好的办法是随机选择范围内的一个数作为 key,避免复杂度升高,这种情况还是属于少数
        int v = arr[left];
        int i = left + 1, j = right;
        while (true) {
            while (i <= right && arr[i] <= v) i ++;
            while (j >= left + 1 && arr[j] >= v) j --;
            if (i >= j) break;
            swap(arr, i, j);
            i ++;
            j --;
        }
        swap(arr, left, j);
        return j;
    }
    private void swap(int[] arr, int i, int j) {
        int v = arr[i];
        arr[i] = arr[j];
        arr[j] = v;
    }
}

30. 连续子数组的最大和

见链接

1.解法一:动态规划

最大子数组的和一定是由当前元素和之前最大连续子数组的和叠加在一起形成的,因此需要遍历 n 个元素,看看当前元素和其之前的最大连续子数组的和能否创造新的最大值。

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        int[] dp = new int[array.length];
        dp[0] = array[0];
        int max = dp[0];
        for (int i = 1;i < dp.length;i++) {
            // 新的最大值肯定是由当前元素之前的最大连续子数组和+当前元素生成的
            dp[i] = Math.max(dp[i - 1] + array[i], array[i]);
            max = Math.max(dp[i], max);
        }
        return max;
    }
}

2.解法二:迭代,空间复杂度为 O(n)

  1. 最大连续子数组的和不一定从第一个元素开始
  2. 累加当前元素之前的所有数字的和,如果小于当前元素并且之前的和小于0,那么最大连续子数组肯定不经过前面
  3. 于是更新临时和以及最大元素
  4. 否则,累加并判断与 max 的关系,更新 max
public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if (array.length == 1) return array[0];
        int max = 0, tmpSum = 0;
        for (int i = 0;i < array.length;i ++) {
            if (tmpSum < array[i] && tmpSum < 0) {
                max = array[i];
                tmpSum = array[i];
            } else {
                tmpSum += array[i];
                if (tmpSum > max) max = tmpSum;
            }
        }
        return max;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值