《剑指offer》各编程题Java版分析 -- 高质量的代码

168 阅读8分钟

题目16 -- 数值的整数次方

题目:实现函数 double Power(double base, int exponent)。求 base 的 exponent 次方。不得使用库函数,同时不需要考虑大数问题。

这一章看起来都是关注代码质量的题,即题目本身不难解决,但是边界条件、异常情况等处处埋雷。

例如这道题,

  1. 首先应该考虑的就是大数问题,如果题目中没有提到可以忽略大数问题,那么我们还需要实现一个 大数类型的乘法

  2. 其次需要考虑的就是0^2,0^(-2),5^(-2),这些情况,即base=0时、exponent是负数时的情况。综合考虑这些后得到代码。

  3. 最后,在求解乘方运算时,可以想到用 二分法 降低时间复杂度。

image.png

public class Solution {

    public double power(double base, int exponent) {
        boolean negative = exponent < 0;
        if (exponent == 0) {
            return 1; // note: include 0^0
        }
        if (base == 0) {
            return 0; // note: include 0^(-2)
        }
        int unsignedExponent = Math.abs(exponent);
        double rawResult = powerWithUnsignedExponent(base, unsignedExponent);
        return negative ? (1 / rawResult) : rawResult;
    }

    private double powerWithUnsignedExponent(double base, int unsignedExponent) {
        if (unsignedExponent == 1) {
            return base;
        }
        int halfExponent = unsignedExponent >> 1;
        double halfResult = powerWithUnsignedExponent(base, halfExponent);
        if ((unsignedExponent & 1) == 1) {
            return base * halfResult * halfResult;
        } else {
            return halfResult * halfResult;
        }
    }

}

题目17 -- 打印从1到最大的n位数

题目:输入数字n,按顺序打印从1到最大的n位十进制数。比如输入3,则打印出1、2、3一直到最大的三位数999。

和上面的题类似,此题的坑在于边界条件的考虑,即考虑大数问题,如果数字n足够大,那么不论是int、long还是long long,都无法抗住这么大的数字。所以需要实现 用数组模拟数字的加法 ,或者 用数组的全排列

用数组模拟数字的加法

不进位

进位不溢出

interview17_1.gif

进位溢出

interview17_2.gif

public class Solution {

    public void printToMaxOfDigits(int n) {
        if (n <= 0) {
            return;
        }
        int[] number = new int[n];
        while (increment(number)) {
            printNumber(number);
        }
    }

    private void printNumber(int[] number) {
        boolean isBeginning0 = true;
        for (int i = 0; i < number.length; i++) {
            if (isBeginning0 && number[i] != 0) {
                isBeginning0 = false;
            }
            if (!isBeginning0) {
                System.out.print(number[i]);
            }
        }
        System.out.println();
    }

    private boolean increment(int[] number) {
        for (int i = number.length - 1; i >= 0; i--) {
            if (number[i] + 1 == 10) {
                number[i] = 0;
            } else {
                number[i]++;
                return true;
            }
        }
        return false;
    }
}

全排列DFS递归树

数组的全排列(递归)

public class Solution2 {

    public void printToMaxOfDigits(int n) {
        if (n <= 0) {
            return;
        }
        int[] number = new int[n];
        printToMaxOfDigitsRecursively(number, 0);
    }

    private void printToMaxOfDigitsRecursively(int[] number, int index) {
        if (index == number.length) {
            printNumber(number);
            return;
        }
        for (int i = 0; i < 10; i++) {
            number[index] = i;
            printToMaxOfDigitsRecursively(number, index+1);
        }
    }

    private void printNumber(int[] number) {
        boolean isBeginningZero = true;
        for (int i = 0; i < number.length; i++) {
            if (isBeginningZero && number[i] != 0) {
                isBeginningZero = false;
            }
            if (!isBeginningZero) {
                System.out.print(number[i]);
            }
        }
        if (!isBeginningZero) {
            System.out.println();
        }
    }
}

面试题18 -- 删除链表的节点

题目一:在O(1)的时间内删除链表节点

题目一:在O(1)的时间内删除链表节点。
给定单向链表的头指针和一个节点指针,定义一个函数在O(1)的时间内删除该节点。

interview18.gif

public class Solution {

    private static class ListNode {
        int value;
        ListNode next;
        ListNode(int value) {
            this.value = value;
        }
    }

    // require caller to ensure nodeToBeDeleted is in listHead.
    public void deleteNode(ListNode listHead, ListNode nodeToBeDeleted) {
        if (listHead == null || nodeToBeDeleted == null || listHead.next == null) {
            return;
        }
        if (nodeToBeDeleted.next == null) {
            ListNode node = listHead;
            // if nodeToBeDeleted is tail, we have to iterate whole list
            while(node.next != null) {
                if (node.next == nodeToBeDeleted) {
                    node.next = null;
                    return;
                }
                node = node.next;
            }
            throw new IllegalStateException("nodeToBeDeleted is not in list!");
        } else {
            ListNode nextNode = nodeToBeDeleted.next;
            nodeToBeDeleted.value = nextNode.value;
            nodeToBeDeleted.next = nextNode.next;
            nextNode.next = null; // not important
        }
    }

    private static void printNode(ListNode head) {
        ListNode node = head.next;
        while (node != null) {
            System.out.print(node.value + " --> ");
            node = node.next;
        }
        System.out.println("null");
    }

    public static void main(String[] args) {
        ListNode head = new ListNode(0);
        head.next = new ListNode(1);
        head.next.next = new ListNode(2);
        head.next.next.next = new ListNode(3);
        printNode(head);
        Solution solution = new Solution();
        solution.deleteNode(head, head.next.next);
        printNode(head);
        solution.deleteNode(head, head.next.next);
        printNode(head);
        solution.deleteNode(head, head.next);
        printNode(head);
    }
}

题目二:删除链表中重复的节点

题目二:删除链表中的重复节点
在一个排序的链表中,如何删除重复的节点?例如,在图中重复的节点被删除后,链表如下。

ListNode

  1. 动手前想好测试用例,写在注释上
  2. 标注好特殊入参todo
  3. 构思实现方式并写出代码原型
  4. 补充细节
  5. 回写todo,脑内编译执行测试用例,脑内debug

本题重点在于控制好三个指针

three-pointers

public void deleteDuplicateNode(ListNode head) {
    if (head == null || head.next == null) {
        return;
    }
    ListNode preNode = head;
    ListNode node = head.next;
    while(node != null) {
        boolean findDuplicate = false;
        ListNode nextNode = node.next;
        while(nextNode != null && nextNode.value == node.value) {
            findDuplicate = true;
            nextNode = nextNode.next;
        }
        // conjunct node and nextNode if find duplicated
        if (findDuplicate) {
            node.next = nextNode;
        }
        node = node.next;
        preNode = preNode.next;
    }
}

面试题19 -- 正则表达式匹配

题目:请实现一个函数用来匹配包含'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但与"aa.a"和"ab*a"均不匹配。

此题是一个状态机(State Machine)问题,以字符串"aaa"匹配正则表达式"ab*ac*a"的过程为例。

正则表达式ab*ac*a的状态机可以表示为如下:

state machine

在正则匹配的过程中可能会遇到这几种情况:

  • # 匹配到 #*
    • 如果字符串input的#与pattern的#相等
      • 当做匹配了一次#*,字符串input向前+1,模式串pattern向前+2(跳过#*) ==> 状态2转向状态3
      • 匹配了一次#*后,还要继续匹配多次,字符串input向前+1,pattern不动 ==> 状态2原地踏步
      • #*匹配失败,即匹配0次,字符串input不动,模式串pattern向前+2 ==> 状态2转向状态3
    • 如果字符串input的#与pattern的#不相等
      • 只能当做pattern的#*匹配了0次,字符串input不动,模式串pattern向前+2 ==> 状态2转向状态3
  • # 匹配 #比较是否相等
public class Solution {

    public boolean matchPattern(String input, String pattern) {
        if (input == null || pattern == null) {
            return false;
        }
        return matchPatternCore(input, 0, pattern, 0);
    }

    private boolean matchPatternCore(String input, int inputIndex, String pattern, int patternIndex) {
        if (inputIndex == input.length() && patternIndex == pattern.length()) {
            return true;
        }
        if (inputIndex < input.length() && patternIndex == pattern.length()) {
            return false;
        }
        if (pattern.charAt(patternIndex + 1) == '*') {
            if (inputIndex < input.length() && isEqual(input.charAt(inputIndex), pattern.charAt(patternIndex))) {
                // 1. move on the next state
                // 2. stay on the current state
                // 3. ignore 'a*'
                return matchPatternCore(input, inputIndex + 1, pattern, patternIndex + 2)
                        || matchPatternCore(input, inputIndex + 1, pattern, patternIndex)
                        || matchPatternCore(input, inputIndex, pattern, patternIndex + 2);
            } else {
                // not equal(include input is over), we can only try ignore
                return matchPatternCore(input, inputIndex, pattern, patternIndex + 2);
            }
        } else if (isEqual(input.charAt(inputIndex), pattern.charAt(patternIndex))) {
            return matchPatternCore(input, inputIndex + 1, pattern, patternIndex + 1);
        } else {
            return false;
        }
    }

    private boolean isEqual(char ch, char patternChar) {
        return ch == patternChar || patternChar == '.';
    }

}

此外还有一种动态规划的解法,是将递归的过程反过来,构建一个二维数组dp,dp[i][j]表示input直到i为止与pattern直到j为止是否匹配,通过各种情况的判断(包括遇到*后的处理逻辑),逐渐补完dp数组,计算出结果。

leetcode题解 - 逐行详细讲解,由浅入深,dp和递归两种思路

面试题20 -- 表示数值的字符串

题目:请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100"、"5e2"、"-123"、"3.1416"及"-1E-16"都表示数值,但"12e"、”1a3.14“、"1.2.3"、"+-5"及"12e+5.4"都不是。

书中的解法思路如下,以'.'和'e'为分隔符,将字符串分割为如下A、B、C三部分,分别判断各个部分是否为合法数字。

official solution

具体的实现细节不一定要真的执行split方法切割字符串,而是通过遍历字符串,在遇到非法字符之前都是A的部分,在遇到'.'或者'E'后,之后的合法字符都作为B或C的部分,最终通过index是否好好遍历完整个字符串作为判断条件。

public class Solution {

    private int index = 0;

    public boolean isNumber(String s) {
        if (s == null || s.length() == 0) {
            return false;
        }
        s = s.trim();
        boolean numeric = scanInteger(s);
        if (index < s.length() && s.charAt(index) == '.') {
            index++;
            numeric = scanUnsignedInteger(s) || numeric;
        }
        if (index < s.length() && isExponent(s.charAt(index))) {
            index++;
            numeric = numeric && scanInteger(s);
        }
        return numeric && index == s.length();
    }

    private boolean scanInteger(String s) {
        if (index < s.length() && isPlusMinus(s.charAt(index))) {
            index++;
        }
        return scanUnsignedInteger(s);
    }

    private boolean scanUnsignedInteger(String s) {
        int before = index;
        while(index < s.length() && isDigit(s.charAt(index))) {
            index++;
        }
        return index > before;
    }

    private boolean isDigit(char c) {
        return c >= '0' && c <= '9';
    }

    private boolean isPlusMinus(char c) {
        return c == '+' || c == '-';
    }

    private boolean isExponent(char c) {
        return c == 'e' || c == 'E';
    }
}

leetcode题解中提到了一种基于 状态机 的解决办法,个人感觉更利于理解,即无脑列举出所有可能的状态转移就完事了。

面试题20. 表示数值的字符串(有限状态自动机,清晰图解)

image.png

面试题21 -- 调整数组顺序使奇数位于偶数前面

题目:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

通过经验很容易联想到快排的partition方法,把奇数与偶数进行分堆,通过先驱节点i遍历nums数组,把奇数的扔左边,把偶数的扔右边。(与书中的左右两个指针向中间靠拢的本质原理是类似的,书中提到将 (nums[i] & 1) == 1 抽象出来 函数指针 便于扩展,这点在Java中可以使用lambda表达式实现)

interview21.gif

public class Solution {

    // normal: 1 2 3 4 5
    // all odd: 1 3 5
    // all even: 2 4 6
    public int[] exchange(int[] nums) {
        if (nums == null || nums.length == 0) {
            return nums;
        }
        int firstEven = 0;
        for (int i = 0; i < nums.length; i++) {
            if ((nums[i] & 1) == 1) { // odd
                swap(nums, i, firstEven);
                firstEven++;
            }
        }
        return nums;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

}

面试题22 -- 链表中倒数第k个节点

题目:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第三个节点是值为4的节点。

很简单的一道题,正如书中所述,重点在于考虑鲁棒性

kth

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    // 1 -> 2 -> 3 -> 4 -> 5 -> 6, k=3
    // 1, k=3
    public ListNode getKthFromEnd(ListNode head, int k) {
        if (head == null || k <= 0) {
            return null;
        }
        ListNode node = head;
        for (int i = 0; i < k; i++) {
            if (node == null) {
                return null; // size of list is less than k.
            }
            node = node.next;
        }
        ListNode kthNode = head;
        while(node != null) {
            node = node.next;
            kthNode = kthNode.next;
        }
        return kthNode;
    }
}

面试题23 -- 链表中环的入口节点

题目:如果一个链表中包含环,如何找出环的入口节点?例如,在如图所示的链表中,环的入口节点是节点3 。

image.png

public class Solution {

    private static class ListNode {
        int val;
        ListNode next;
        ListNode(int x) { val = x; }
    }

    public ListNode entryNodeOfLoop(ListNode head) {
        if (head == null) {
            return null;
        }
        ListNode meetingNode = findMeetingNode(head);
        if (meetingNode == null) {
            return null; // means there is no loop in list
        }
        int nodeCountOfLoop = getNodeCountOfLoop(meetingNode);
        ListNode foreNode = head;
        for (int i = 0; i < nodeCountOfLoop; i++) {
            foreNode = foreNode.next;
        }
        ListNode behindNode = head;
        while(foreNode != behindNode) {
            foreNode = foreNode.next;
            behindNode = behindNode.next;
        }
        return behindNode;
    }

    private ListNode findMeetingNode(ListNode head) {
        // assert head is not null
        ListNode slow = head;
        ListNode fast = head;
        while(slow != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                return slow;
            }
        }
        return null;
    }

    private int getNodeCountOfLoop(ListNode meetingNode) {
        // assert meetingNode is a loop which means there's no null in list
        ListNode node = meetingNode.next;
        int i = 1;
        while(node != meetingNode) {
            i++;
            node = node.next;
        }
        return i;
    }
}

面试题24 -- 反转链表

题目:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

本题的重点在控制好before、node、after这三个指针,node用于遍历,before用于在反转的时候设置node.next=before,after用于在node调整之后还能找到下一个节点,使遍历继续下去。

image.png

public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode before = null;
    ListNode node = head;
    ListNode after = head.next;
    while(node != null) {
        node.next = before;
        if (after == null) {
            return node;
        }
        before = node;
        after = after.next;
        node = after;
    }
    throw new IllegalStateException("not supposed to be here.");
}

面试题25 -- 合并两个排序的链表

题目:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

image.png

同样是一道题目并不难,但是需要控制好指针的调整,防止出现指针指向不符合预期的bug。用循环和递归都可以解决。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null || l2 == null) {
        return l1 == null ? l2 : l1;
    }
    ListNode baseList = l1.val < l2.val ? l1 : l2;
    ListNode branchList = baseList == l1 ? l2 : l1;
    ListNode baseNode = baseList.next;
    ListNode foreNode = baseList;
    ListNode branchNode = branchList;
    while(baseNode != null && branchNode != null) {
        if (baseNode.val <= branchNode.val) {
            foreNode = foreNode.next; // baseNode & foreNode walk forward
            baseNode = baseNode.next;
        } else {
            foreNode.next = branchNode; // merge branchNode into baseList and walk forward
            branchNode = branchNode.next;
            foreNode.next.next = baseNode;
            foreNode = foreNode.next;
        }
    }
    if (branchNode != null) {
        foreNode.next = branchNode;
    }
    return baseList;
}

public ListNode mergeTwoListsInBook(ListNode l1, ListNode l2) {
    if (l1 == null || l2 == null) {
        return l1 == null ? l2 : l1;
    }
    ListNode mergedHead;
    if (l1.val < l2.val) {
        mergedHead = l1;
        mergedHead.next = mergeTwoListsInBook(l1.next, l2);
    } else {
        mergedHead = l2;
        mergedHead.next = mergeTwoListsInBook(l1, l2.next);
    }
    return mergedHead;
}

面试题26 -- 树的子结构

题目:输入两颗二叉树A和B,判断B是不是A的子结构。

image.png

此题理解的重点在于理解题意,B是A的子结构=B的结构能够在A的任意一个位置对上

interview26.gif

public boolean isSubStructure(TreeNode A, TreeNode B) {
    if (B == null) {
        return false;
    } else if (A == null) {
        return false;
    }
    // pre-order traverse to find if B is-root-substructure of A
    return isRootSubStructure(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}

private boolean isRootSubStructure(TreeNode A, TreeNode B) {
    if (B == null) {
        return true;
    } else if (A == null) {
        return false;
    }
    return A.val == B.val && isRootSubStructure(A.left, B.left) && isRootSubStructure(A.right, B.right);
}