文章目录
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种匹配方式:
- 模式后移2字符,相当于x*被忽略;
- 字符串后移1字符,模式后移2字符,相当于x*匹配一位;
- 字符串后移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;
}
}