牛客剑指offer:题解(51-60)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


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]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * ... * A[n-1]B[n-1] = A[0] * A[1] * ... * A[n-2];

1.解法一:暴力求解,时间复杂度O(n^2)

  1. 使用两次循环,外层循环代表要给B 的第i个位置赋值
  2. 内层循环记录A的乘积,乘积不包括A[i],题目要求
public class Solution {
    public int[] multiply(int[] A) {
        if (A == null) return A;
        int len = A.length;
        int[] B = new int[len];
        if (len <= 1) return B;
        for (int i = 0;i < len;i ++) {
            int res = 1;
            for (int j = 0;j < len;j ++) {
                if (i != j)
                    res *= A[j];
            }
            B[i] = res;
        }
        return B;
    }
}

2.解法二:优化一中有大量重复的子问题

根据AB 的关系B[i] = A[0] *...*A[i - 1] * A[i + 1] * ... * A[n - 1] ,我们可以把这个关系看成两个部分的乘积,其中C[i] = A[0] *...*A[i - 1]D[i] = A[i + 1] * ... * A[n - 1],则最终B[i] = C[i] * D[i] ,这个i代表的是行数

那么我们可以得到如下一个矩阵来形象化,B[i]为矩阵中第i行的所有元素之积

根据上面的图,我们可以得出B[i] = C[i] * D[i],其中C[0] = 1,C[i] = C[i - 1] * A[i - 1]D[len - 1] = 1,D[i] = D[i + 1] * A[i + 1]

使用空间来换时间,将时间复杂度提升为O(n)

public class Solution {
    public int[] multiply(int[] A) {
        if (A == null || A.length < 1) return A;
        int len = A.length;
        int[] B = new int[len];
        int[] C = new int[len]; // C 记录左半部分
        int[] D = new int[len]; // D 记录右半部分
        // 先计算 C
        C[0] = 1;
        for (int i = 1;i < len;i ++) 
            C[i] = C[i - 1] * A[i - 1];
        // 再计算 D
        D[len - 1] = 1;
        // 从倒数第二行开始往上
        for (int i = len - 2;i >= 0;i --)
            D[i] = D[i + 1] * A[i + 1];
        // 最后计算 B
        for (int i = 0;i < len;i ++)
            B[i] = C[i] * D[i];
        return B;
    }
}

3.解法三:对解法二一点点小改动,无需额外空间了,解法二比较直观易懂

public class Solution {
    public int[] multiply(int[] A) {
        if (A == null || A.length < 1) return A;
        int len = A.length;
        int[] B = new int[len]; // 让B先记录左边的值
        B[0] = 1;
        for (int i = 1;i < len;i ++) 
            B[i] = B[i - 1] * A[i - 1];
        int temp = 1; // 用 temp 来记录右边的值
        for (int i = len - 2;i >= 0;i --) { // 右边的值可以自下向上
            temp *= A[i + 1]; // 边记录变更新
            B[i] *= temp; // 并且一并更新 B
        }
        return B;
    }
}

52.正则表达式匹配

描述见链接

1.解法一:递归

参考博文

  1. 当用一个字符去和模式串中的字符进行匹配时,如果模式中的字符是.,那么任何字符都可以匹配;或者,如果两个字符相同,那么可以匹配,接着再去匹配下一个字符;
  2. 当模式串的第二个字符不是*, 问题就比较简单:若字符串的第一个字符和模式串的第一个匹配时,字符串和模式串指针都向后移动一个字符,然后匹配剩余的字符串和模式串。如果第一个字符不匹配,那么就直接返回false
  3. 当模式串的第二个字符是*时, 可能就出现多种不同的匹配方式:
    1. 无论第一个字符是否相等,模式串向后移动两个字符,相当于*和他前面的字符被忽略,因为*可以代表前面一个字符出现0次;
    2. 如果模式串中第一个字符和字符串中第一个字符匹配,则字符串向后移动一个字符,比较下一位,而此时模式串有两种情况:
      1. 模式串向后移动两个字符;
      2. 保持模式串不变(因为*代表前面的字符出现多次)
public class Solution {
    public boolean match(char[] str, char[] pattern) {
        if (str == null || pattern == null) return false;
        return match(str, 0, pattern, 0);
    }
    private boolean match(char[] str, int i, char[] pattern, int j) {
        if (i == str.length && j == pattern.length) return true;	// 都为空
        if (i <  str.length && j == pattern.length) return false;	// 模式串为空
        // 以下情况中,j < pattern.length
        if (j + 1 < pattern.length && pattern[j + 1] == '*') {	// 第二个字符是 *
            if ((i < str.length && str[i] == pattern[j]) 
                || (i < str.length && pattern[j] == '.')) {
                // 第一个字符相等有三种情况,分别匹配0,1,或多个
                return match(str, i, pattern, j + 2) 
                    || match(str, i + 1, pattern, j + 2) 
                    || match(str, i + 1, pattern, j); 
            } else {	// 第一个字符不相等
                return match(str, i, pattern, j + 2);
            }
        } else {	// 第二个字符不是 *
            if ((i < str.length && str[i] == pattern[j]) 
                || (i < str.length && pattern[j] == '.')) {
                return match(str, i + 1, pattern, j + 1);
            } else {
                return false;
            }
        }
    }
}

53.表示数值的字符串

描述见链接

首先要清楚数值的字符串模式包括什么?

  1. 第一种情况:A [ . [ B ] ] [ e | E C]
  2. 第二种请况:. B [ e | E C]

其中A 是数值的整数部分(可以没有,此时小数部分一定要有),B为跟在小数点后的小数部分(可以没有),C为跟在eE之后的指数部分。并且AC都可能以+ -开头,B前面不能有符号

1.解法一:正则表达式

正则表示式

看的这个,自己不太会

public class Solution {
    public boolean isNumeric(char[] str) {
        String s = String.valueOf(str);
        //+代表出现一次或多次,*代表出现0次或多次,?代表出现0次或者一次
        String regexp = "[\\+-]?[0-9]*(\\.[0-9]+)?([eE][\\+-]?[0-9]+)?";
        return s.matches(regexp);
    }
}

2.解法二:遍历字符,挨个匹配(未做)

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

描述见链接

1.解法一:使用一个长度为256的字符数组来存储可能出现的字符

import java.lang.StringBuilder;
public class Solution {
    private StringBuilder sb = new StringBuilder();
    private int[] arrCount = new int[256];
    //Insert one char from stringstream
    public void Insert(char ch) {
        sb.append(ch);
        arrCount[ch] ++;
    }
    //return the first appearence once char in current stringstream
    public char FirstAppearingOnce() {
        for (int i = 0;i < sb.length();i ++) {
            char c = sb.charAt(i);
            // 去这个arrCount数组中查看这个字符出现的次数
            if (arrCount[c] == 1)
                return c;
        }
        return '#';
    }
}

55.链表中环的入口节点

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

1.解法一:使用哈希集合来存储节点

import java.util.HashSet;
public class Solution {
    public ListNode EntryNodeOfLoop(ListNode pHead) {
        // 使用HashSet存储节点,出现过的那个节点就是环的入口
        if (pHead == null || pHead.next == null) return null;
        HashSet<ListNode> set = new HashSet<>();
        while (pHead != null) {
            if (set.contains(pHead))
                return pHead;
            set.add(pHead);
            pHead = pHead.next;
        }
        return null;
    }
}

2.解法二:Floyd 算法

public class Solution {
    // 使用快慢指针,先判断有没有环,再判断环的入口
    public ListNode EntryNodeOfLoop(ListNode pHead) {
        if (pHead == null || pHead.next == null) return null;
        ListNode slow = pHead;
        ListNode fast = pHead;
        ListNode meet = null;
        // 先判断有没有环
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                meet = fast;
                break;
            }
        }
        // 如果为空,说明没有环
        if (meet == null) return null;
        // 重新从起点出发
        slow = pHead;
        while (meet != slow) {
            meet = meet.next;
            slow = slow.next;
        }
        return meet;
    }
}

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

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

1.解法一

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        if (pHead == null || pHead.next == null) return pHead;
        ListNode dummy = new ListNode(-1);
        dummy.next = pHead;
        // prev 已经确保不会重复的最后一个
        ListNode prev = dummy, curr = pHead;
        boolean flag = false;	// 标记是否重复
        while (prev != null && curr != null) {
            flag = false;
            // 有重复的就前进,并把标记置为true
            while (curr.next != null && curr.next.val == curr.val) {
                curr = curr.next;
                flag = true;
            }
            // 循环出来后还要再往后跳一个到未重复的元素去
            if (flag && curr != null) {
                curr = curr.next;
            }
            // 搞掉一个重复的还不能连接,因为下一个是否重复还要再接着判断
            if (curr == null || curr.next == null || curr.next.val != curr.val) {
                prev.next = curr;
                prev = curr;
                if (curr != null)	// 防止空指针
                    curr = curr.next;
            }
        }
        return dummy.next;
    }
}

2.解法二

public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        if (pHead == null || pHead.next == null) return pHead;
        ListNode dummy = new ListNode(-1);
        dummy.next = pHead;
        ListNode prev = dummy, curr = pHead;
        while (curr != null) {
            if (curr.next != null && curr.val == curr.next.val) {
                while (curr.next != null && curr.val == curr.next.val)
                    curr = curr.next; // 将curr移动到最后一个相等的元素
                // 更新 prev 到下一个不重复的节点
                prev.next = curr.next;
                curr = curr.next;
            } else {
                prev = prev.next;
                curr = curr.next;
            }
        }
        return dummy.next;
    }
}

57.二叉树的下一个节点

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

1.解法一:先找到根节点,进行中序遍历,再去找目标节点的下一个节点

  1. 时间复杂度O(n)
  2. 空间复杂度O(n)
public class Solution {
    private ArrayList<TreeLinkNode> list = new ArrayList<>();
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        // 先找到父节点,中序遍历父节点,找到目标节点的下一个节点
        if (pNode == null) return pNode;
        TreeLinkNode par = pNode; 
        // 一直向上找到根节点
        while (par.next != null)
            par = par.next;
        inOrder(par);	// 中序遍历根节点
        for (int i = 0;i < list.size();i ++) {
            if (pNode == list.get(i)) 
                // 如果是最后一个节点,返回空节点
                return i == list.size() - 1 ? null : list.get(i + 1);
        }
        return null;
    }
    // 中序遍历这棵树
    private void inOrder(TreeLinkNode node) {
        if (node != null) {
            inOrder(node.left);
            list.add(node);
            inOrder(node.right);
        }
    }
}

2.解法二:直接去找目标节点的下一个节点

参考来源

直接去找目标节点中序遍历的下一个节点可以分为三种情况:

  1. 该节点有右子树,那么他的下一个节点就是右子树中的最左边的节点;如节点B
  2. 该节点没有右子树,但是他是他的父节点的左节点,那么这个父节点就是他的下一个中序遍历节点。如节点H
  3. 该节点没有右子树,并且还是他的父节点的右节点,那么我们就一直沿着父节点追溯,直到找到某个节点N是其父节点的左子树,如果存在这样的节点N,那么这个节点N的父节点就是我们要找的下一个节点。如节点I ,下个节点是A ,(I -> E -> B -> A)

图源:牛客网

public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        if (pNode == null) return pNode;
        // 有右子树
        if (pNode.right != null) {
            TreeLinkNode pRight = pNode.right;
            while (pRight.left != null)
                pRight = pRight.left;
            return pRight;
        }
        // 无右子树,且节点是该节点父节点的左子树
        // 首先保证父节点要存在
        if (pNode.next != null && pNode == pNode.next.left) {
            return pNode.next;
        }
        // 无右子树且节点是该节点父节点的右子树
        if (pNode.next != null) {
            TreeLinkNode pNext = pNode.next;
            while (pNext.next != null && pNext.next.right == pNext)
                pNext = pNext.next;
            return pNext.next;
        }
        return null;
    }
}

58.对称的二叉树

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

1.解法一:递归

对称二叉树: 根节点的左右子树相同,左子树的左子树和右子树的右子树相同,左子树的右子树和右子树的左子树相同

public class Solution {
    boolean isSymmetrical(TreeNode pRoot) {
        if (pRoot == null)    return true;
        return isSymmetrical(pRoot.left, pRoot.right);
    }
    private boolean isSymmetrical(TreeNode left, TreeNode right) {
        // 左右孩子为空满足
        if (left == null && right == null)    return true;
        // 左右孩子有一个不为空,另一个为空,不满足
        if (left == null || right == null)    return false;
        // 左右孩子值不同,不满足
        if (left.val != right.val)    return false;
        // 递归判断	左子树的左子树和右子树的右子树
        // 递归判断 左子树的右子树和右子树的左子树
        return isSymmetrical(left.left, right.right)
            && isSymmetrical(left.right, right.left);
    }
}

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

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

1.解法一:层序遍历时加一个正序还是反序打印的标志位

  1. 在偶数层进行reverse操作可能在海量数据下效率很低
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if (pRoot == null) return res;
        // 使用一个LinkedList 充当队列
        LinkedList<TreeNode> q = new LinkedList<>();
        q.add(pRoot);
        boolean flag = true;    // 代表正序还是反序打印
        while (!q.isEmpty()) {
            int size = q.size();
            ArrayList<Integer> level = new ArrayList<>();
            for (int i = 0;i < size;i ++) {
                // removeFirst() 取得队首元素
                TreeNode top = q.removeFirst();
                level.add(top.val);
                if (top.left != null) q.add(top.left);
                if (top.right != null) q.add(top.right);
            }
            if (flag == true) {
                res.add(level);
            } else {
                // 如果是反向打印,先把层逆序一下
                Collections.reverse(level);
                res.add(level);
            }
            // 更改标志位
            flag = !flag;
        }
        return res;
    }
}

2.解法二:优化一,不使用reverse操作。使用两个栈来操作

值得注意的地方:

  1. 14行和29行,先判断弹出来的节点node是否为空,因为在压栈的过程中会把空节点null给一起压入栈中
  2. 1732 行,压栈的顺序可以体现出最后出栈是从左往右出栈,还是从右往左出栈
  3. 2338行,先判断层级链表temp是否为空,可以避免结果集中出现空链表[]
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {       
        if (pRoot == null)    return new ArrayList<>();
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        Stack<TreeNode> stack1 = new Stack<>();    // 存奇数层
        Stack<TreeNode> stack2 = new Stack<>();    // 存偶数层
        boolean flag = true;	
        stack1.push(pRoot);    // 根节点在奇数层
        while (!stack1.empty() || !stack2.empty()) {
            if (flag) {
                ArrayList<Integer> temp = new ArrayList<>();
                while (!stack1.empty()) {
                    TreeNode node = stack1.pop();
                    // 因为可能压入栈的是null,所以先判断一下,避免空指针异常
                    if (node != null) {
                        temp.add(node.val);
                        // 从左往右推进去,从右往左弹出来
                        stack2.push(node.left);
                        stack2.push(node.right);
                    }
                }
                // 不是空链表才添加到结果集中
                if (!temp.isEmpty())    
                    res.add(temp);
            } else {
                ArrayList<Integer> temp = new ArrayList<>();
                while (!stack2.empty()) {
                    TreeNode node = stack2.pop();
                    // 因为可能压入栈的是null,所以先判断一下,避免空指针异常
                    if (node != null) {
                        temp.add(node.val);
                        // 从右往左推进去,从左往右弹出来
                        stack1.push(node.right);
                        stack1.push(node.left);
                    }
                }
                // 不是空链表才添加到结果集中
                if (!temp.isEmpty())    
                    res.add(temp);
            }
            flag = !flag;    // 标记下一次是正序还是反序
        }
        return res;
    }
}

60.把二叉树打印成多行

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

1.解法一:层序遍历即可

public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        // 首先跟面试官沟通好当根节点为空,返回 [] 还是 [[]]    
        if (pRoot == null) return res;
        // 使用一个队列
        LinkedList<TreeNode> q = new LinkedList<>();
        q.add(pRoot);
        while (!q.isEmpty()) {
            int size = q.size();
            ArrayList<Integer> level = new ArrayList<>();
            for (int i = 0;i < size;i ++) {
                TreeNode node = q.removeFirst();
                level.add(node.val);
                if (node.left != null) q.add(node.left);
                if (node.right != null) q.add(node.right);
            }
            res.add(level);
        }
        return res;
    } 
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值