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

Note

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

目录

题号 题目名称
51 构建乘积数组
52 正则表达式匹配
53 表示数值的字符串
54 字符流中第一个不重复的字符
55 链表中环的入口节点
56 删除链表中重复的节点
57 二叉树的下一个结点
58 对称的二叉树
59 按之字形顺序打印二叉树
60 把二叉树打印成多行

正文

51、构建乘积数组

题目描述

给定一个数组A[0,1,…,n-1],请构建一个数组B[0,1,…,n-1],其中B中的元素B[i]=A[0]*A[1]*…*A[i-1]A[i+1]…*A[n-1]。不能使用除法。

题目分析

解法一: 使用两个数组mul1和mul2,经过一次遍历分别存储前n个数字的乘积和后n个数字的乘积,生成结果数组时,只需要分别取出mul1和mul2中对应的数字相乘即可。时间复杂度:O(n) 空间复杂度:O(n)
解法二: 暴力法,两层for循环强行算出乘积。未实现。时间复杂度:O(n²) 空间复杂度:O(n)

代码实现

解法一: O(n)

public int[] multiply(int[] A) {
    if (A == null || A.length < 2) return null;
    int[] mul1 = new int[A.length];
    int[] mul2 = new int[A.length];
    mul1[0] = A[0];
    mul2[A.length - 1] = A[A.length - 1];
    for (int i = 1, j = A.length - 2; i < A.length; i++, j--) {
        mul1[i] = mul1[i - 1] * A[i];
        mul2[j] = mul2[j + 1] * A[j];
    }
    int[] B = new int[A.length];
    for (int i = 0; i < B.length; i++) {
        if (i == 0) {
            B[i] = mul2[i + 1];
        } else if (i == B.length - 1) {
            B[i] = mul1[i - 1];
        } else {
            B[i] = mul1[i - 1] * mul2[i + 1];
        }
    }
    return B;
}

52、正则表达式匹配

题目描述

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

题目分析

解法一: 递归。先看*再看匹配。首先,当pattern遍历完时,如果str恰好遍历完则返回true,否则返回false。如果pattern的当前字符存在下一个字符,则看下一个字符是否是*,如果是,有两种情况:
1、当前str和pattern匹配。pattern的下一个字符*可能用来匹配多个str的当前字符,此时str后移;或者pattern的下一个字符*不用来匹配str的任何字符,此时pattern后移两位;
2、当前str和pattern不匹配,pattern后移两位即可。
如果下一个字符不是*,则str和pattern都后移一位。在比较过程中需要考虑pattern为’.‘的情况,但是’.‘只可能占一位比较字符,所以在判断匹配时多加一个pattern[j]==‘.’的条件即可。
解法二: 动态规划。dp[i][j]设置为boolean数组,它代表着str[i]及其以后的字符和pattern[j]及其以后的字符的匹配情况。我们对两个字符串从后往前进行遍历,判断pattern当前字符的下一个是否是’*’,如果下一个是’*‘且当前字符相等,则dp[i][j]=dp[i][j+2] || dp[i+1][j];如果下一个是’*'且当前字符不等,则dp[i][j]=dp[i][j+2] ;如果下一个不是‘*’且当前字符相等,则d[i][j]= dp[i + 1][j + 1]。

代码实现

解法一:

public boolean match(char[] str, char[] pattern) {
    if (str == null || pattern == null) {
        return false;
    }
    return helper(str, 0, pattern, 0);
}

public boolean helper(char[] str, int i, char[] pattern, int j) {
    if (j == pattern.length) {
        return i == str.length;
    }
    //pattern下一个字符是*
    if ((j < pattern.length - 1) && (pattern[j + 1] == '*')) {
        //当前字符匹配
        if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
            return helper(str, i + 1, pattern, j) || helper(str, i, pattern, j + 2);
        } else {
            return helper(str, i, pattern, j + 2);
        }
    } else {
        if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
            return helper(str, i + 1, pattern, j + 1);
        }
    }
    return false;
}

解法二: O(mn)

public boolean match(char[] str, char[] pattern) {
    if(str == null || pattern == null)
        return false;
    boolean [][] dp = new boolean[str.length + 1][pattern.length + 1];
    dp[str.length][pattern.length] = true;
    for (int i = str.length; i >= 0; i--) {
        for (int j = pattern.length - 1; j >= 0; j--) {
            if (j < pattern.length - 1 && pattern[j + 1] == '*') {
                //下一个是'*'且当前字符相等
                if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
                    dp[i][j] = dp[i][j + 2] || dp[i + 1][j];
                } else { //下一个是'*'且当前字符不等
                    dp[i][j] = dp[i][j + 2];
                }
            } else { //下一个不是'*'且当前字符相等
                if (i < str.length && (str[i] == pattern[j] || pattern[j] == '.')) {
                    dp[i][j] = dp[i + 1][j + 1];
                }
            }
        }
    }
    return dp[0][0];
}

53、表示数值的字符串

题目描述

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

题目分析

解法一: 分类讨论。
±号后面必定为数字或后面为.(-.123 = -0.123)
±号只出现在第一位或在eE的后一位
.后面必定为数字或为最后一位(233. = 233.0)
eE后面必定为数字或±号
解法二: 正则表达式。

代码实现

解法一: O(n)

public boolean isNumeric(char[] str) {
    boolean point = false;
    boolean exp = false;  //标识小数点和指数,只能出现一次
    for (int i = 0; i < str.length; i++) {
        if (str[i] == '+' || str[i] == '-') {
            if (i + 1 == str.length || !(str[i + 1] >= '0' && str[i + 1] <= '9' || str[i + 1] == '.')) {
                return false;
            }
            if (!(i == 0 || str[i-1] == 'e' || str[i-1] == 'E')) {
                return false;
            }
        } else if (str[i] == '.') {
            if (point || exp || !(i + 1 < str.length && str[i + 1] >= '0' && str[i + 1] <= '9')) {
                return false;
            }
            point = true;
        } else if (str[i] == 'e' || str[i] == 'E') {
            if (exp || i + 1 == str.length || !(str[i + 1] >= '0' && str[i + 1] <= '9' || str[i + 1] == '+' || str[i + 1] == '-')) {
                return false;
            }
            exp = true;
        } else if (str[i] < '0' || str[i] > '9') {
            return false;
        }
    }
    return true;
}

解法二:

public boolean isNumeric1(char[] str) {
    String pattern = "^[-+]?\\d*(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?$";
    return Pattern.matches(pattern, new String(str));
}

54、字符流中第一个不重复的字符

题目描述

请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。

输出描述

如果当前字符流没有存在出现一次的字符,返回#字符。

题目分析

解法一: 使用两个长度为256的数组模拟哈希表,一个用于流中字符的出现顺序,一个用于存储字符的出现次数。获得第一个不重复字符时,对记录出现次数的数组进行遍历,找到出现次数为1且出现顺序最小的那个字符并返回。
解法二: 使用两个哈希表。LinkedHashSet是输入有序的,用于按出现顺序保存不重复的字符;另一个HashSet用来保存出现次数大于1的字符。在Insert时对LinkedHashSet中的字符进行去重操作,需要取数据时,只需要取出LinkedHashSet中的第一个字符即可。

代码实现

解法一: O(n)

int[] count = new int[256]; // 字符出现的次数
int[] index = new int[256]; // 字符出现的次数
int number = 0;

public void Insert1(char ch) {
    count[ch]++;
    index[ch] = number++;
}

public char FirstAppearingOnce1() {
    int minIndex = number;
    char ch = '#';
    for (int i = 0; i < 256; i++) { 
        if (count[i] == 1 && index[i] < minIndex) {
            ch = (char) i;
            minIndex = index[i];
        }
    }
    return ch;
}

解法二: O(n)

Set<Character> set1 = new LinkedHashSet<>();
Set<Character> set2 = new HashSet<>();
public void Insert(char ch) {
    if (!set1.contains(ch) && !set2.contains(ch)) {
        set1.add(ch);
    } else {
        set1.remove(ch);
        set2.add(ch);
    }
}

public char FirstAppearingOnce() {
    if (set1.isEmpty()) {
        return '#';
    } else {
        char t = set1.iterator().next();
        return t;
    }
}

55、链表中环的入口节点

题目描述

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

题目分析

解法一: 快慢指针法。快指针每次走两步,慢指针每次走一步,如果快指针能够走到null说明无环。若有环,快慢指针必定在某个节点meetNode相遇。和‘求倒数第k个节点’问题类似,此时将一个指针指向链表头部,一个指针指向meetNode,两个指针每次都只走一步,最终它们相遇的点即为环的入口节点。

代码实现

解法一: O(n)

public ListNode EntryNodeOfLoop(ListNode pHead) {
    ListNode meetNode = meetNode(pHead);
    if (meetNode == null) return null;
    ListNode p1 = pHead;
    ListNode p2 = meetNode;
    while (p1 != p2) {
        p1 = p1.next;
        p2 = p2.next;
    }
    return p1;
}

public ListNode meetNode(ListNode pHead) {
    ListNode pSlow = pHead;
    ListNode pFast = pHead;
    while (pSlow != null && pFast != null) {
        pSlow = pSlow.next;
        pFast = pFast.next;
        if (pFast!= null) {
            pFast = pFast.next;
        }
        if (pSlow != null && pFast != null && pSlow == pFast) {
            return pSlow;
        }
    }
    return null;
}

56、删除链表中重复的节点

题目描述

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。

题目分析

解法一: 遍历链表。由于头结点也可能会重复,所以需要新建一个头结点。然后对链表进行遍历,pre指针永远指向cur的前一个节点,用于跳过重复节点。cur指针不断往后遍历,判断后续节点是否重复,重复则cur后移。最后判断cur是否移动过,如果没有移动过说明中间没有重复节点,则pre指向cur,cur后移;如果移动过说明中间有重复节点,pre的next指向cur的next。一定要记住:出现重复的话,pre指针一定不能直接后移!不然出现像“2->2->3->3”这种连续重复的情况,就无法通过pre删除重复节点了。

代码实现

解法一: O(n)

public static ListNode deleteDuplication(ListNode pHead) {
    if (pHead == null) return pHead;
    ListNode dummy = new ListNode(-1);
    dummy.next = pHead;
    ListNode pre = dummy, cur = pHead;
    while (cur != null) {
        while (cur.next != null && cur.val == cur.next.val) {
            cur = cur.next;
        }
        if (pre.next == cur) {
            pre = cur;
        } else {
            pre.next = cur.next;
        }
        cur = cur.next;
    }
    return dummy.next;
}

57、二叉树的下一个结点

题目描述

给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

题目分析

解法一: 中序遍历特性。对于中序遍历而言。如果某个结点有右孩子,则遍历的下一个结点是它的右孩子的最左结点;如果某个节点没有右孩子,则遍历的下一个结点是它的某个作为左孩子的祖先结点。根据以上逻辑便能够写出代码,需要注意的是遍历过程中的最后一个结点,它的下一个结点为null。

代码实现

解法一: O(n)

public TreeLinkNode GetNext(TreeLinkNode pNode) {
    if (pNode == null) return null;
    if (pNode.right != null) {
        pNode = pNode.right;
        while (pNode.left != null) {
            pNode = pNode.left;
        }
        return pNode;
    } else {
        while (pNode.next != null && pNode.next.right == pNode) {
            pNode = pNode.next;
        }
        return pNode.next;
    }
}

58、对称的二叉树

题目描述

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

题目分析

解法一: 递归。子问题其实就是左子树的孩子和右子树的孩子是否对称,对称的实质就是左子树的左孩子等于右子树的右孩,左子树的右孩子等于右子树的左孩子。每次递归的pLeft和pRight都会指向同一层级的两个对称节点,然后对其左右子树进行递归的对称判断。

代码实现

解法一:

boolean isSymmetrical(TreeNode pRoot) {
    if (pRoot == null) return true;
    return helper(pRoot.left, pRoot.right);
}

boolean helper(TreeNode pLeft, TreeNode pRight) {
    if (pLeft == null && pRight == null) return true;
    if (pLeft == null || pRight == null) return false;
    if (pLeft.val == pRight.val) {
        return helper(pLeft.left, pRight.right) && helper(pLeft.right, pRight.left);
    } else {
        return false;
    }
}

59、按之字形顺序打印二叉树

题目描述

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

题目分析

解法一: 双栈法。本题的关键点有两个:如何判断当前层遍历完成;如何逆序打印。本解法使用两个栈,每个栈只存一层的节点,通过栈空来判断当前层的遍历是否完成;使用栈来完成逆序打印。
具体做法:根据层序遍历的思路,使用两个栈,轮流压栈和出栈。例如遍历第一行时,将第二行压入栈2,此时左右孩子压入的顺序是先左后右,则第二行遍历时会从右往左打印;遍历第二行时,将第三行压入栈1,此时左右孩子压入的顺序是先右后左,则第三行遍历时会从左往右打印。使用一个bool变量来标识此时的打印方向,用于判断此时应该压入哪个栈以及从哪个栈弹出。
解法二: BFS。依然是层序遍历思想,根据解法一中所提及的两个关键点。本解法通过队列长度来判断当前层是否遍历完成;通过list.add(T)完成顺序插入,通过list.add(0, T)将结果插入至表头,完成逆序插入。
具体做法:先得到队列长度size,该size就是当前层的节点个数,然后通过for循环将该层的所有节点poll出来,然后通过list.add(T)和list.add(0, T)完成list的插入。也可以不使用list.add(0, T),而是顺序插入,直到当前层插入完毕后使用Collection.reverse()对数组进行倒序。

代码实现

解法一:

public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> list = new ArrayList<>();
    Stack<TreeNode> stack1 = new Stack<>();
    Stack<TreeNode> stack2 = new Stack<>();
    boolean isRight = true;
    if (pRoot == null) return list;
    stack1.push(pRoot);
    ArrayList<Integer> tList = new ArrayList<>();
    while (!stack1.isEmpty() || !stack2.isEmpty()) {
        if (isRight && !stack1.isEmpty()) {
            TreeNode t = stack1.pop();
            tList.add(t.val);
            if (t.left != null) stack2.push(t.left);
            if (t.right != null) stack2.push(t.right);
            if (stack1.isEmpty()) {
                list.add(tList);
                tList = new ArrayList<>();
                isRight = false;
            }
            continue;
        }
        if (!isRight && !stack2.isEmpty()) {
            TreeNode t = stack2.pop();
            tList.add(t.val);
            if (t.right != null) stack1.push(t.right);
            if (t.left != null) stack1.push(t.left);
            if (stack2.isEmpty()) {
                list.add(tList);
                tList = new ArrayList<>();
                isRight = true;
            }
            continue;
        }
    }
    return list;
}

解法二:

public ArrayList<ArrayList<Integer> > Print1(TreeNode pRoot) {
    LinkedList<TreeNode> linkedList = new LinkedList<>();
    ArrayList<ArrayList<Integer>> res = new ArrayList<>();
    boolean isRight = true;
    linkedList.add(pRoot);
    while (!linkedList.isEmpty()) {
        int size = linkedList.size();
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode node = linkedList.poll();
            if (node == null) continue;
            if (isRight) {
                list.add(node.val);
            } else {
                list.add(0, node.val);
            }
            linkedList.offer(node.left);
            linkedList.offer(node.right);
        }
        if (list.size() != 0) {
            res.add(list);
        }
        isRight = !isRight;
    }
    return res;
}

60、把二叉树打印成多行

题目描述

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

题目分析

解法一: 层序遍历。使用队列进行层序遍历,每次记录当前层的size,使用for循环将队列出队,并加入下一层节点,当for循环结束代表当前层的遍历已经结束,将当前数组加入结果数组中。

代码实现

解法一:

ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> list = new ArrayList<>();
    if (pRoot == null) return list;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(pRoot);
    while (!queue.isEmpty()) {
        int size = queue.size();
        TreeNode t;
        ArrayList<Integer> tList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            t = queue.poll();
            tList.add(t.val);
            if (t.left != null) {
                queue.offer(t.left);
            }
            if (t.right != null) {
                queue.offer(t.right);
            }
        }
        list.add(tList);
    }
    return list;
}

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