剑指Offer/51-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]。不能使用除法。(注意:规定B[0] = A[1] * A[2] * … * A[n-1],B[n-1] = A[0] * A[1] * … * A[n-2];)
对于A长度为1的情况,B无意义,故而无法构建,因此该情况不会存在。

解题思路:
在这里插入图片描述

可以看到,B[i]的左边和B[i-1]有关,右边和B[i+1]有关。
B[i]=A[0] * A[1]… * A[i-1]乘上A[i+1]…A[n-1] 以A[i]为分界线,将B分为左右两个部分相乘。
B[i]=左 * 右 利用一轮循环计算所有的左下三角,一轮循环计算所有的右上三角
当把A[0]…A[n-2]利用一轮for循环算出来时,左下角的所有乘积都知道了(得到了n-1个值),同理一轮for循环,右上角的A[1]…A[n-1]所有乘积都知道了(也得到了n-1个值),因此在第两轮for循环之时,进行对应的“组装”就可以得到对应的B[i]的值,相当于两轮循环下来,就可以完成所有计算。

public class Solution {
    public int[] multiply(int[] A) {
        if (A == null || A.length < 2){
            return null;
        }
        int[] B = new int[A.length];
        B[0] = 1;
        //B数组中暂时存放了自己对应左下三角的乘积
        for(int i=1; i<A.length; i++){
            B[i] = B[i-1]*A[i-1];
        }
        
        int temp = 1;
        for(int i=A.length-2; i>=0; i--){
            temp *= A[i+1];//temp就是i位置对应的右边部分的值,从A[n-1]一直乘到A[1]
            B[i] *= temp;//左边部分乘以右上部分
        }
        return B;
    }
}

52. 正则表达式匹配

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

解题思路
思路:当模式中的第二个字符是“*”时:
如果字符串第一个字符跟模式第一个字符不匹配,则模式后移2个字符,继续匹配。如果字符串第一个字符跟模式第一个字符匹配,可以有3种匹配方式:

  1. 模式后移2字符,相当于x*被忽略;
  2. 字符串后移1字符,模式后移2字符,相当于x*匹配一位;
  3. 字符串后移1字符,模式不变,即继续匹配字符下一位,相当于x匹配多位;
    当模式中的第二个字符不是“
    ”时:
    如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的部分。
    如果字符串第一个字符和模式中的第一个字符相不匹配,直接返回False。
public class Solution {
    public boolean match(char[] str, char[] pattern){
        if(str == null || pattern == null)
            return false;
        return matchCore(str, 0, pattern, 0);
    }
    //.表示任意一个字符,*表示它前面的字符可以出现任意次(包含0次)
    //两个指针分别指向 str 和 pattern
    private boolean matchCore(char[] str, int s, char[] pattern, int p){
        //下面四行是递归结束标志,两个指针都指到了最后才是匹配
        if(s == str.length && p == pattern.length)
            return true;
        if(s < str.length && p == pattern.length)
            return false;
        //虽然比的是p位置,但是p后面出现*时,规则需要改变
        //p 后面出现的字符为 *,而且第一个字符匹配,边界为模式指针未达到末尾
        if(p+1 < pattern.length && pattern[p+1] == '*'){
            //出现了*,并且 s 和 p 指向的相同,3种情况并列
                //matchCore(str, s, pattern, p+2) 模式没有出现
                //matchCore(str, s+1, pattern, p+2) 模式出现了一次
                //matchCore(str, s+1, pattern, p) 模式出现了多次(至少大于1次)
            if((s < str.length && pattern[p] == '.') || (s < str.length && pattern[p] == str[s])){
                return matchCore(str, s, pattern, p+2) 
                        || matchCore(str, s+1, pattern, p) || matchCore(str, s+1, pattern, p+2);
             }else{
                //第一个字符不匹配,pattern直接移动两位
                return matchCore(str, s, pattern, p+2);
            }
        }
        //p 后面出现的不是 *,且当前字符匹配,那么就进行常规判断,相同就分别给指针+1
        if(s < str.length && (pattern[p] == str[s] || pattern[p] == '.'))
            return matchCore(str, s+1, pattern, p+1);
        //第一个字符不匹配
        return false;
    }
}

53. 表示数值的字符串

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

解题思路:模式匹配

import java.util.regex.Pattern;
public class Solution {
    public boolean isNumeric(char[] str) {
        //^和$框定正则表达式,表示对文本中所有的字符都进行匹配
        //如果仅包含^,将匹配以一个数字开头的字符串
        //如果仅包含$,将匹配以一个数组结尾的字符串
        //[+-]?正负号后面的?表示这个符号是可选的,表示有0到1个负号或者正号
        //\d的含义和[0-9]一样,它匹配一个数字,后缀*指引它可匹配0个或者多个数字
        //(?:\\.\\d*)?
        //(?:...)? 表示一个可选的非捕获型分组。*指引这个分组会匹配后面跟随的0个或多个数字的小数点
        //(?:[eE][+\\-]?\d+)?
        //这是另一个可选的非捕获型分组,它会匹配一个e(E)、一个可选的正负号以及一个或多个数字
        //   * 匹配前面的子表达式零次或多次
        //   + 匹配前面的子表达式一次或多次
//         String pattern = "^[+-]?\\d*(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?$";
        String pattern = "^[+-]?\\d*(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$";
        String s = new String(str);
        return Pattern.matches(pattern, s);
    }
}

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

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

解题思路:
方法1:
  只使用一个数组,数组中不仅可以判断是否出现了多次,并且在出现一次时,数组中存放字符在字符流中出现第一次的位置,而数组索引和字符的ASCII码对应起来,由于ASCII码有128个字符,因此数组大小为128即可。
  将数组初值赋值-1,某个字符第一次出现时,将-1更新成当前字符在字符流中的索引(位置)。第二次出现时,将之前赋值的索引擦除,并且不用进入后续的判断,这里选择赋值成-2(赋值情况不唯一)。也就是说,没出现过,数组记录-1,出现一次,数组记录位置,出现多次,数组记录成-2.
  而在FirstAppearingOnce方法中,我们要遍历数组找到正确的解,所以我们要做两件事,第1件事是找到所有出现1次的字符,由于要求我们返回第一次出现一次的字符,所以第2件事就是比较各个出现1次的字符在字符流中的位置,我们要找到最先出现的那个字符即位置最小的字符。因此还需要设置一个变量,该变量记录某个字符的位置,在找到下一个出现一次的字符时,比较这两个只出现1次的字符的位置,看谁更靠前,就要谁。(可以把这一过程理解成,在某个数组中找到最小的那个数,这个数就是第一个出现1次的字符的索引)
  
方法2:
之前是一个数组又可以记录索引信息,又可以记录是否出现了多次,用两个数组,将表达的信息分解,所以代码看起来没有解法1那么绕。并且还和解法1不同的是,index记录的不再是第一次出现的位置,而是记录某个字符最后出现的位置,但其实没什么影响,因为我们也用count记录次数,出现1次的字符,记录的位置仍然正确(只出现1次的位置既是第一次出现也是最后一次出现)

方法3:
利用LinkedHashMap,因为LinkedHashMap底层是一个有序的双向链表

//方法1:开辟一个数组空间
public class Solution {
    private int index = 0;//index记录某个字符出现的位置
    private int[] arr = new int[128];//ASCII码有128个字符
    //数组初始化
    public Solution(){
        for(int i = 0; i<128; i++){
            arr[i] = -1;
        }
    }
    //Insert one char from stringstream
    public void Insert(char ch){
        if(arr[ch] == -1){//arr[ch] 与 arr[(int)ch]一样
            arr[ch] = index;    //第一次出现时,记录其在字符流中的位置
        }else if(arr[ch] >= 0){//大于0,说明某个字符出现过了
            arr[ch] = -2;    //多次出现时,重置
        }
        index++;
    }
  //return the first appearence once char in current stringstream
    public char FirstAppearingOnce(){
        //方便比较出最靠前的那个出现一次的字符
        int minIndex = Integer.MAX_VALUE;
        char ch = '#';
        for(int i = 0; i < 128; i++){
            if(arr[i] >= 0 && arr[i] < minIndex){
                //字符赋值给ch,位置赋值为minIndex
                ch = (char)i;
                minIndex = arr[i];
            }
        }
        return ch;
    }
}

//方法2:开辟两个数组空间
// public class Solution {
//     int[] count = new int[128];//字符出现的次数
//     int[] index = new int[128];//字符出现的位置
//     int number = 0;//标记字符流的位置
//     public void Insert(char ch){
//         count[ch]++;
//         index[ch] = number++;
//     }
//     public char FirstAppearingOnce(){
//         int minIndex = number;//minIndex 用于记录仅出现一次的字符的最小位置
//         char ch = '#';
//         for(int i = 0; i < 128; i++){
//             if(count[i] == 1 && index[i] < minIndex){
//                 ch = (char)i;
//                 minIndex = index[i];
//             }
//         }
//         return ch;
//     }
// }

//方法3:利用LinkedHashMap
// public class Solution {
//     Map<Character, Integer> map = new LinkedHashMap<>();
//     public void Insert(char ch){
//         if(map.containsKey(ch)){
//             map.put(ch, map.get(ch)+1);//value记录的是次数
//         }else{
//             map.put(ch, 1);//第一次出现
//         }
//     }
//     public char FirstAppearingOnce(){
//         for(char ch: map.keySet()){
//             if(map.get(ch) == 1){//遍历,找出次数为1的
//                 return ch;
//             //由于LinkedHashMap的底层实现是双向链表,
//                 //所以在遍历中,找到的第一个为1的就是答案
//             }
//         }
//         return '#';
//     }
// }

55. 链表中环的入口结点

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

解题思路:
方法1:利用集合
(1) 遍历单链表的每个结点
(2) 如果当前结点地址没有出现在set中,则存入set中
(3) 否则,出现在set中,则当前结点就是环的入口结点
(4) 整个单链表遍历完,若没出现在set中,则不存在环

方法2:双指针
(1) 初始化:快指针fast指向头结点, 慢指针slow指向头结点
(2) 让fast一次走两步, slow一次走一步,第一次相遇时停止
(3) 然后让fast指向头结点,slow原地不动,然后fast,slow每次走一步,当再次相遇,就是入口结点。

/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
import java.util.HashSet;
public class Solution {
    public ListNode EntryNodeOfLoop(ListNode pHead){
//         HashSet<ListNode> set = new HashSet<>();
//         ListNode temp = pHead;
//         while(temp != null){
//             if(set.contains(temp)){
//                 return temp;
//             }else{
//                 set.add(temp);
//                 temp = temp.next;
//             }
//         }
//         return null;
        
        //方法2:双指针的方法
        if(pHead == null || pHead.next == null){
            return null;
        }
        ListNode fast = pHead;
        ListNode slow = pHead;
        do{
            fast = fast.next.next;
            slow = slow.next;
        }while(fast != slow);
        fast = pHead;
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return fast;
    }
}

补充:判断链表中是否存在环

public class Solution {
	public boolean hasCycle(ListNode head) {
		ListNode fast = head;
		ListNode slow = head;
		while(fast != null && fast.next != null){
			fast = fast.next.next;
			slow = slow.next;
			if(fast == slow){
				return true;
			}
		}
		return false;
	}
}

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

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

解题思路:直接删除法(遍历的同时删除)
借助辅助头结点,可避免单独讨论头结点的情况。设置两个结点 slow 和 fast,当 fast 和 fast.next 值相等,fast 一直向前走,直到不等退出循环,这时候 fast 指的值还是重复值,调整 fast 和 slow 的指针再次判断.

/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode deleteDuplication(ListNode pHead){
        if(pHead == null || pHead.next == null){
            return pHead;
        }
        ListNode head = new ListNode(0);//新建一个头结点,防止链表中头结点是重复节点被删除
        head.next = pHead;
        ListNode fast = head.next;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            if(fast.val == fast.next.val){
                while(fast.next != null && fast.val == fast.next.val){
                    fast = fast.next;
                }
                slow.next = fast.next;
                fast = fast.next;
            }else{
                fast = fast.next;
                slow = slow.next;
            }
        }
        return head.next;
    }
}

57. 二叉树的下一个结点

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

解题思路:
在这里插入图片描述

仔细分析,可以把中序下一结点归为几种类型:
(1) 有右子树,下一结点是右子树中的最左结点,例如 B,下一结点是 H
(2) 无右子树,且结点是该结点父结点的左子树,则下一结点是该结点的父结点,例如 H,下一结点是 E
(3) 无右子树,且结点是该结点父结点的右子树,则我们一直沿着父结点追朔,直到找到某个结点是其父结点的左子树,如果存在这样的结点,那么这个结点的父结点就是我们要找的下一结点。例如 I,下一结点是 A;例如 G,并没有符合情况的结点,所以 G 没有下一结点

/*
public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}
*/
import java.util.ArrayList;
public class Solution {
    static ArrayList<TreeLinkNode> list = new ArrayList<>();
    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;
        }
        //若没有右子树,则返回的是父节点
        while(pNode.next != null){
            //父节点的左结点等于本身,且本身没有右节点,那么直接返回父节点
            if(pNode.next.left == pNode){
                return pNode.next;
            }
            //父节点的左结点不等于本身,说明本身在父节点的右子节点,
            //    继续遍历父结点的父节点
            pNode = pNode.next;
        }
        return null;
        
        
//         //暴力方法——还原二叉树
//         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;
//     }
//     public void InOrder(TreeLinkNode pNode){
//         if(pNode != null){
//             InOrder(pNode.left);
//             list.add(pNode);
//             InOrder(pNode.right);
//         }
    }
}

58. 对称的二叉树

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

解题思路:递归

(1) 递归函数功能:输入两个指针,左右指针分别按中左右,中右左遍历树。如果为对称树,返回true,否则返回false。
(2) 递归终止条件:如果两个指针都为空,返回true。否则如果两指针不同时为空,或两指针值不相等,或下一步递归结果为false,返回false。
(3) 下一步递归:当两指针不为空且值相等时,左右指针分别按左右,右左的顺序访问下一个结点

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    boolean isDuiChen(TreeNode leftRoot, TreeNode rightRoot){
        if(leftRoot == null && rightRoot == null)
            return true;
        if(leftRoot == null || rightRoot == null)
            return false;
        if(leftRoot.val == rightRoot.val){
            return isDuiChen(leftRoot.left, rightRoot.right) && isDuiChen(leftRoot.right, rightRoot.left);
        }else{
            return false;
        }
    }
    boolean isSymmetrical(TreeNode pRoot){
        if(pRoot == null)
            return true;
        return isDuiChen(pRoot.left, pRoot.right);
    }
}

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

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

解题思路:队列
层次遍历的方式,与层次遍历的唯一区别就是按照奇数层,从左到右打印,偶数层,从右到左打印。

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;
    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        if(pRoot == null){
            return result;
        }
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(pRoot);
        //设置boolean变量控制从左到右还是从右到左
        //boolean reverse = false;
        int high = 1;
        while(!queue.isEmpty()){
            int size = queue.size();
            ArrayList<Integer> list = new ArrayList<>();
            while(size > 0){
                TreeNode node = queue.poll();
//                 if(reverse == false){
                if(high % 2 == 1){//奇数层,正序;偶数层,逆序
                    list.add(node.val);
                }else{
                    list.add(0,node.val);//每次加到0的位置,就自动逆序了
                }
                if(node.left != null){
                    queue.offer(node.left);
                }
                if(node.right != null){
                    queue.offer(node.right);
                }
                size--;
            }
            if(list.size() > 0){//在此题中,若是不加判断会无法通过
                result.add(list);
            }
            high++;
//             reverse = !reverse;
        }
        return result;
    }
}

60. 把二叉树打印成多行

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

解题思路:队列(层次遍历)
每次出队一个元素,就将该元素的孩子节点加入队列中,直至队列中元素个数为0时,出队的顺序就是该二叉树的层次遍历结果。

/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if(pRoot == null){
            return res;
        }
        ArrayList<Integer> list;
        Queue<TreeNode> queue = new LinkedList<>();
        TreeNode cur;
        queue.add(pRoot);
        while(! queue.isEmpty()){
            list = new ArrayList<>();
            int size = queue.size();
            while(size > 0){//记录上一层的size
                cur = queue.poll();
                list.add(cur.val);
                if(cur.left != null){
                    queue.offer(cur.left);
                }
                if(cur.right != null){
                    queue.offer(cur.right);
                }
                size--;
            }
            if(list.size() > 0){
                res.add(list);
            }
        }
        return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值