剑指offer——java刷题总结【三】

Note

  • 题解汇总:剑指offer题解汇总
  • 代码地址:Github 剑指offer Java实现汇总
  • 点击目录中的题名链接可直接食用题解~
  • 有些解法博文中未实现,不代表一定很难,可能只是因为博主太懒```(Orz)
  • 如果博文中有明显错误或者某些题目有更加优雅的解法请指出,谢谢~

目录

题号 题目名称
21 栈的压入、弹出序列
22 从上往下打印二叉树
23 二叉搜索树的后序遍历序列
24 二叉树中和为某一值的路径
25 复杂链表的复制
26 二叉搜索树与双向链表
27 字符串的排列
28 数组中出现次数超过一半的数字
29 最小的K个数
30 连续子数组的最大和

正文

21、栈的压入、弹出序列

题目描述

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

题目分析

解法一: 设置一个指向pop数组的index指针,遍历push数组,针对每一个遍历值做如下操作:
1、将当前遍历值压入栈中;
2、查看栈顶元素和pop数组的当前弹出值是否相等,如果相等则模拟出栈操作,将stack的栈顶元素弹出,并对pop数组的指针进行后移。循环此过程直至两元素不相等,说明栈顶元素弹出不满足当前的pop序列;
3、如果最终栈空,则说明我们定义的栈成功模拟了pop数组的出栈顺序,返回true;如果栈不为空,则说明无法按照pop数组的顺序出栈,返回false。

代码实现

解法一: O(n)

public static boolean IsPopOrder(int [] pushA,int [] popA) {
    Stack<Integer> stack = new Stack<>();
    int index = 0;
    for (int i = 0; i < pushA.length; i++) {
        stack.push(pushA[i]);
        while (!stack.isEmpty() && stack.peek() == popA[index]) {
            index++;
            stack.pop();
        }
    }
    return stack.isEmpty();
}

22、从上往下打印二叉树

题目描述

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

题目分析

解法一: 层序遍历。层序遍历其实就是广度遍历,使用队列辅助实现;深度遍历需要使用栈辅助实现。如果具体实现中,我们将返回类型定义为ArrayList,就可以不需要开辟额外的队列空间,直接用数组加index指针完成层序遍历,将空间复杂度从O(n)优化为O(1)。

代码实现

解法一: O(n)

public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
    ArrayList<Integer> list = new ArrayList<>();
    if (root == null) return list;
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.add(root);
    while(!queue.isEmpty()) {
        root = queue.poll();
        list.add(root.val);
        if (root.left != null) {
            queue.add(root.left);
        }
        if (root.right != null) {
            queue.add(root.right);
        }
    }
    return list;
}

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

题目描述

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

题目分析

解法一: 递归实现。我们需要判断二叉搜索树的后续遍历序列,对于后序遍历序列而言,序列的最后一个元素一定为根节点。且对于二叉搜索树而言,左子树的元素一定比根节点小,右子树的元素一定比根节点大。因此对于合法的二叉搜书的后续遍历序列,可以先将当前序列的最后一个元素设置为根节点,我们一定能够在序列中找到一个中点,中点的左半部分全部小于等于最后一个元素,中点的右半部分全部大于等于最后一个元素。如果找不到这个中点,则说明这是不合法的序列,返回false。最后递归地对中点划分出的左右子树进行同样的求解,直到子数组元素小于等于2个说明无需再分,返回true。
解法二: 最大最小边界约束法。只是看到有这种方法,暂未实现,据说可以达到O(n)的时间复杂度,感兴趣可自行学习。

代码实现

解法一: O(nlogn)

public boolean verify(int[] array, int begin, int end) {
    if (end - begin < 2) return true;
    int key = array[end];
    int i;
    for (i = begin; i < end; i++) {
        if (array[i] > key) break;
    }
    for (int j = i + 1; j < end; j++) {
        if (array[j] < key) return false;
    }
    return verify(array, begin, i - 1) && verify(array, i, end - 1);
}

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

题目描述

输入一颗二叉树的根节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

题目分析

解法一: 递归实现。根据前序遍历的思想对树进行遍历,当到达叶子节点时且该条路径的值符合条件时,将整条路径添加到result中。注意,需要使用result.add(new ArrayList<>(list))而不能使用result.add(list),因为list只是一个引用,result中的数据会被list后续的数据更新所影响。而使用result.add(new ArrayList<>(list))是将list重新拷贝了一份加入result中。在左右孩子的递归结束后,需要将list中在当前层次添加的节点移除,避免路径重复。
解法二: 非递归实现。使用栈进行深度优先遍历,暂未实现。

代码实现

解法一: O(logn)

public ArrayList<ArrayList<Integer>> result = new ArrayList<ArrayList<Integer>>();
public ArrayList<Integer> list = new ArrayList<>();
public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
    tranverse(root, target);
    return result;
}
public void tranverse(TreeNode root, int target) {
     if (root == null) {
         return;
    }
    list.add(root.val);
    if (target == root.val && root.left == null && root.right == null) {
        result.add(new ArrayList<>(list));
    }
    FindPath(root.left, target - root.val);
    FindPath(root.right, target - root.val);
    list.remove(list.size() - 1);
    return;
}

25、复杂链表的复制

题目描述

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

题目分析

解法一: 在原链表的基础上,在每个链表节点后new出一个label相同的新节点;然后重新遍历链表,复制原节点上的random给新节点;最后将两个链表拆开。
解法二: 使用HashMap存储新旧节点的对应关系,再次遍历链表取出相应的新节点。暂未实现。

代码实现

解法一: O(n)

public static RandomListNode Clone(RandomListNode pHead) {
    if (pHead == null) {
        return null;
    }

    RandomListNode currentNode = pHead;
    //1、复制每个结点,如复制结点A得到A1,将结点A1插到结点A后面;
    while (currentNode != null){
        RandomListNode cloneNode = new RandomListNode(currentNode.label);
        RandomListNode nextNode = currentNode.next;
        currentNode.next = cloneNode;
        cloneNode.next = nextNode;
        currentNode = nextNode;
    }

    currentNode = pHead;
    //2、重新遍历链表,复制老结点的随机指针给新结点,如A1.random = A.random.next;
    while (currentNode != null) {
        currentNode.next.random = currentNode.random == null ? null : currentNode.random.next;
        currentNode = currentNode.next.next;
    }

    //3、拆分链表,将链表拆分为原链表和复制后的链表
    currentNode = pHead;
    RandomListNode pCloneHead = pHead.next;
    while (currentNode != null) {
        RandomListNode cloneNode = currentNode.next;
        currentNode.next = cloneNode.next;
        cloneNode.next = cloneNode.next == null ? null : cloneNode.next.next;
        currentNode = currentNode.next;
    }

    return pCloneHead;
}

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

题目描述

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

题目分析

解法一: 二叉搜索树的中序遍历就是有序序列,因此对二叉搜索树进行中序遍历,将中序遍历的当前节点与前一个节点进行连接。本解法使用栈完成非递归的遍历。
解法二: 递归实现二叉树的中序遍历,将中序遍历的结果依次添加到数组中,然后遍历整个数组,建立双向链表。未实现。

代码实现

解法一: O(n)

public static TreeNode Convert(TreeNode pRootOfTree) {
    if (pRootOfTree == null) {
        return pRootOfTree;
    }
   TreeNode head = pRootOfTree;
   TreeNode tmp = null;
    Stack<TreeNode> stack = new Stack<>();
   while (head != null || !stack.empty()) {
      if (head != null) {
         stack.push(head);
         head = head.left;
      } else {
         TreeNode pop = stack.pop();
         head = pop;
         if (tmp != null) {
            tmp.right = pop;
         }
         pop.left = tmp;
         tmp = pop;
            head = head.right;
      }
   }
    while (tmp.left != null) {
        tmp = tmp.left;
    }
    return tmp;
}

27、字符串的排列

题目描述

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
输出描述
输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

题目分析

解法一: 该题目其实就是对数组中的每个字符和其它字符进行交换完成的,但是该交换过程是递归完成的。以"abc"为例,在第一层递归中,会将第一个字符’a’和后续字符进行依次交换,形成"abc"、"bac"和"cba"传入下一层递归,在下一次递归中,又会分别以整个字符串的第二个字符开始,与后续字符依次交换。当交换到最后一个字符时,说明交换完毕,加入Set中,之所以使用Set是因为题目要求结果是去重且按字典序排序的。
解法二: 该解法只保证最后结果是去重的,我们在交换的过程中加入了哈希表,当交换的两个字符相同时,我们不进行交换操作,以保证结果不重复。但是无法保证结果按字典序排序。

代码实现

解法一: O(n²)

public static ArrayList<String> Permutation(String str) {
    if (str == null || str.length() == 0) {
        return new ArrayList<>();
    }
    Set<String> set = new TreeSet<>();
    helper(str.toCharArray(), 0, set);
    return new ArrayList<>(set);
}

public static void helper(char[] s, int i, Set<String> set) {
    if (i == s.length) {
        set.add(String.valueOf(s));
    }
    for (int j = i; j < s.length; j++) {
        swap(s, i, j);
        helper(s, i + 1, set);
        swap(s, i, j);
    }
}

public static void swap(char[] s, int i, int j) {
    if (i == j) {
        return;
    }
    s[i] ^= s[j];
    s[j] ^= s[i];
    s[i] ^= s[j];
}

解法二: O(n²)

public static void helper(char[] s, int i) {
    if (i == s.length) {
        System.out.println(String.valueOf(s));
    }
    HashSet<Character> set = new HashSet<>();
    for (int j = i; j < s.length; j++) {
        if (!set.contains(s[j])) {
            set.add(s[j]);
            swap(s, i, j);
            helper(s, i + 1);
            swap(s, i, j);
        }
    }
}

public static void swap(char[] s, int i, int j) {
    if (i == j) {
        return;
    }
    s[i] ^= s[j];
    s[j] ^= s[i];
    s[i] ^= s[j];
}

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

题目描述

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

题目分析

解法一: 遍历数组,用哈希表记录每个数字出现的次数,最后把记录表遍历一遍查看是否有大于数组一半的数字。
解法二: 用preValue记录上一次访问的值,count表明当前值出现的次数,如果下一个值和当前值相同那么count++;如果不同count- -,减到0的时候就要更换新的preValue值了,因为如果存在超过数组长度一半的值,那么最后preValue一定会是该值。但是由于可能不存在该数,所以最后还需要重新遍历一遍数组查看所选数是否真的符合条件,未必比解法一快。

代码实现

解法一: O(n)

public static int MoreThanHalfNum_Solution(int [] array) {
    if (array == null || array.length == 0) {
        return 0;
    }
    int halfLen = array.length / 2;
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < array.length; i++) {
        map.put(array[i], map.getOrDefault(array[i], 0) + 1);
    }
    Iterator<Integer> it = map.keySet().iterator();
    while (it.hasNext()) {
        int t = it.next();
        if (map.get(t) > halfLen) {
            return t;
        }
    }
    return 0;
}

解法二: O(n)

public static int MoreThanHalfNum_Solution(int[] array) {
    if (array == null || array.length == 0) {
        return 0;
    }
    int preValue = 0;
    int count = 0;
    for (int i = 0; i < array.length; i++) {
        if (count == 0) {
            preValue = array[i];
            count = 1;
        } else {
            if (array[i] == preValue) {
                count++;
            } else {
                count--;
            }
        }
    }

    int cnt = 0;
    for (int i = 0; i < array.length; i++) {
        if (array[i] == preValue) {
            cnt++;
        }
    }
    return cnt > array.length / 2 ? preValue : 0;
}

29、最小的K个数

题目描述

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

题目分析

解法一: 使用选择排序,选出前k小的数字。
解法二: 利用快排的partition函数,找到某个最终位置为k的pivlot。时间复杂度可达O(nlogk)。未实现。
解法三: 遍历数组,使用大小为k的堆(PriorityQueue),维护到目前为止最小的k个数。时间复杂度可达O(nlogk)。未实现。

代码实现

解法一: O(nk)

public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
    if (input == null || input.length == 0 || k > input.length) {
        return new ArrayList<>();
    }
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < k; i++) {
        int minIndex = i;
        for (int j = i + 1; j < input.length; j++) {
            minIndex = input[j] < input[minIndex] ? j : minIndex;
        }
        list.add(input[minIndex]);
        swap(input, i, minIndex);
    }
    return list;
}

public void swap(int[] arr, int i, int j) {
    if (i == j) return;
    arr[i] ^= arr[j];
    arr[j] ^= arr[i];
    arr[i] ^= arr[j];
}

30、连续子数组的最大和

题目描述

给一个数组,返回它的最大连续子序列的和。(子向量的长度至少是1)
例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)

题目分析

解法一: 典型的动态规划。dp[n]代表以当前元素为截止点的连续子序列的最大和,如果dp[n-1]>0,dp[n]=dp[n]+dp[n-1],因为当前数字加上一个正数一定会变大;如果dp[n-1]<0,dp[n]不变,因为当前数字加上一个负数一定会变小。使用一个变量max记录最大的dp值返回即可。

代码实现

解法一: O(n)

public int FindGreatestSumOfSubArray(int[] array) {
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        array[i] += array[i - 1] > 0 ? array[i - 1] : 0;
        max = Math.max(max, array[i]);
    }
    return max;
}

你可能感兴趣的:(算法)