文章目录

欢迎关注个人​​数据结构专栏​​哈
​剑指offer(1-10题)详解​​​​剑指offer(11-25题)详解​
​剑指offer(26-33题)详解​
​剑指offer(34-40题)详解​
​剑指offer(41-50题)详解​
​剑指offer(51-59题)详解​
​剑指offer(60-67题)详解​
微信公众号:​​bigsai​
声明:大部分题基本未参考题解,基本为个人想法,如果由效率太低的或者错误还请指正!

剑指offer(51-59题)详解_算法

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]​​。不能使用除法。

思路
这题刚开始还没想到,刚开始还想着用啥位运算?刚开始想着怎么用总数变成对应的数,但是人家要求不能用除法。得用乘法。(不要按照公式每个每个的死算,这样太低效)。其实把上面等式右侧看成两部分就行了。​​A[0]*A[1]*...*A[i-1]​​​和​​A[i+1]*...*A[n-1]​​。

在具体处理上,我们使用两个数组,一个数组​​leftmut[]​​记录从左向右的叠乘,一个数组​​rightmut[]​​记录从右向左的叠乘。这样正常情况下每个位置的​​B[i]​​​就变成了​​leftmut[i-1]*rightmut[i+1]​​.当然,特殊情况和边界需要特殊考虑下!

剑指offer(51-59题)详解_算法_02


实现代码为:

import java.util.ArrayList;
public class Solution {
public int[] multiply(int[] A) {
//特殊情况暂不考虑
int len=A.length;
int B[]=new int[len];
if(len<2)return B;

int leftmut[]=new int [len];
int rightmut[]=new int [len];
leftmut[0]=A[0];
rightmut[len-1]=A[len-1];
for(int i=1;i<len;i++)
{
leftmut[i]=A[i]*leftmut[i-1];//从左向右递乘法
rightmut[len-1-i]=A[len-1-i]*rightmut[len-i];//从右向左递乘法
}

B[0]=rightmut[1];
B[len-1]=leftmut[len-2];
for(int i=1;i<len-1;i++)
{
B[i]=leftmut[i-1]*rightmut[i+1];
}
return B;

}
}

52 正则表达式匹配

题目描述

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

思路
这题后面要优化,感觉我写的逻辑太乱了。本来想能不能先把非符号去掉。。能不能dp。。想不出来。进了讨论区瞅了眼发现递归妙用。

遇到相同的或者​​.​​​就往下递归匹配。遇到​​x*​​就分类讨论看看pattern能不能前进(2位),str能不能前进。把所有可能都递归下去(有真就行)。但是感觉这样效率真的不高啊。。。虽然过了。后面代码和逻辑都要优化。

实现代码:

import java.util.Arrays;
public class Solution {
public static boolean match(char[] str, char[] pattern)
{
if(str.length==0)//匹配串为0
{
if(pattern.length==0)return true;
else {
if(pattern.length%2==1)return false;
else {
for(int i=1;i<pattern.length;i++)
if(pattern[i]!='*')return false;
return true;
}
}

}
else if (pattern.length==0) {//匹配串为0,肯定不行
return false;
}
else {
if(pattern.length==1)
{
if(pattern[0]==str[0]||pattern[0]=='.')
{
if(str.length==1)
return true;
}


return false;
}
else {
if(pattern[1]=='*')
{
if(pattern[0]=='.')
{
return match(Arrays.copyOfRange(str, 1, str.length),pattern)||match(str, Arrays.copyOfRange(pattern, 2, pattern.length));
}
if(pattern[0]==str[0])
{
return match(Arrays.copyOfRange(str, 1, str.length), pattern)||match(Arrays.copyOfRange(str, 1, str.length), Arrays.copyOfRange(pattern, 2, pattern.length))||match(str, Arrays.copyOfRange(pattern, 2, pattern.length));
}
else {
return match(str, Arrays.copyOfRange(pattern, 2, pattern.length));
}
}
else {
if (pattern[0]==str[0]||pattern[0]=='.') {
return match(Arrays.copyOfRange(str, 1, str.length), Arrays.copyOfRange(pattern, 1, pattern.length));
}
else {
return false;
}
}
}

}
}
}

​参考讨论区​​ 讨论区的比萨大叔讲的挺好的,另外就是我这里事用数组拷贝来比较,而他用数组指标代替其实效率更高的。空间可以重复利用,并且数组复制占用太多空间还是递归的!!可以参考下!

剑指offer(51-59题)详解_剑指offer_03

链接:https://www.nowcoder.com/questionTerminal/45327ae22b7b413ea21df13ee7d6429c?f=discussion
来源:牛客网

public class Solution {
public boolean match(char[] str, char[] pattern) {
if (str == null || pattern == null) {
return false;
}
int strIndex = 0;
int patternIndex = 0;
return matchCore(str, strIndex, pattern, patternIndex);
}

public boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
//有效性检验:str到尾,pattern到尾,匹配成功
if (strIndex == str.length && patternIndex == pattern.length) {
return true;
}
//pattern先到尾,匹配失败
if (strIndex != str.length && patternIndex == pattern.length) {
return false;
}
//模式第2个是*,且字符串第1个跟模式第1个匹配,分3种匹配模式;如不匹配,模式后移2位
if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
return matchCore(str, strIndex, pattern, patternIndex + 2)//模式后移2,视为x*匹配0个字符
|| matchCore(str, strIndex + 1, pattern, patternIndex + 2)//视为模式匹配1个字符
|| matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个,再匹配str中的下一个
} else {
return matchCore(str, strIndex, pattern, patternIndex + 2);
}
}
//模式第2个不是*,且字符串第1个跟模式第1个匹配,则都后移1位,否则直接返回false
if ((strIndex != str.length && pattern[patternIndex] == str[strIndex]) || (pattern[patternIndex] == '.' && strIndex != str.length)) {
return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
}
return false;
}
}

53 表示数值的字符串

题目描述

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

思路
经典字符串处理题!字符串问题其实还是比较麻烦的。因为可能会有很多纰漏,需要考虑点比较多。这题需要考虑的点大致有:

  • 最多有两个独立数字e(E)前面可为小数或者整数,而后面只能为整数
  • 正负号只能在独生数字开始
  • 除了数字、​​+-.Ee​​其他字符均非法
  • 只有前面有小数点。注意e(E)前后不能为空(小数点也是只不过这题数据较弱没写也过了可以自己添上)

逻辑实现上,可以用一些​​int​​类似数组标记独立数字,小数点数字长度啥的。然后对字符串进行各个分类讨论,判断各个符号出现次数,是否合法等等。详细可以参考代码:

//数据比较弱。 12.e+4   12. 这类需要特殊判断  
public static boolean isNumeric(char[] str) {
boolean smallpoint=false;//小数点
boolean ise=false;//是否遇到e
int localindex=0;//当前数字指标
for(int i=0;i<str.length;i++)
{
if(localindex==0&&(str[i]=='+'||str[i]=='-'))
{
localindex++;continue;
}
else if (str[i]=='.'&&localindex>0) {
if(smallpoint||ise)//当有小数点或者在e后面
return false;
else {
smallpoint=true;
}
}
else if ((str[i]=='e'||str[i]=='E')&&localindex>0) {
if(ise)return false;
else {
ise=true;localindex=0;
}
}
else if (str[i]>='0'&&str[i]<='9') {
localindex++;
}
else {
return false;
}
}
if (localindex>0) {
return true;
}
else
return false;
}

​参考评论区:​

剑指offer(51-59题)详解_链表_04

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

题目描述

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

输出描述:

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

思路
这题首先要理解题意吧。题目就是给了两个操作,​​​insert​​​和​​FirstAppearingOnce​​两个函数,至于一些其他需要你自己实现。你可以选择字符数组、或者String做容器储存。这里我是用StringBuider储存。

insert肯定都没问题,但​​FirstAppearingOnce​​​这个你不要每次都从头开始找,暴力枚举、肯定会炸的。你需要用个字符储存记录。也就是insert同时实现动态查找。而​​FirstAppearingOnce​​返回参数即可。

具体实现,我用的hashmap储存每个出现的次数。用index标记当前位置的出现第一个数。对于插入,如果不等于index位置的数,那么不需要改变第一个只出现次数为1的数,如果插入的是index位置字符,那么就需要从index往后查找下一个,如果index和字符串长度一样,那么就返回​​#​​。

实现代码为:

import java.util.HashMap;
public class Solution {
//Insert one char from stringstream
int index=0;
StringBuilder sb=new StringBuilder();
HashMap<String, Integer>map=new HashMap<String, Integer>();
public void Insert(char ch)
{
sb.append(ch);
if(map.containsKey(ch+"")) {//前面有该元素
map.put(ch+"", 2);
if((index<sb.length())&&ch==sb.charAt(index))//正是第一次出现的
{
for(;index<sb.length();index++)
{
System.out.println(index);
if(map.get(sb.charAt(index)+"")==1)
{
break;
}
}
}
}
else {
map.put(ch+"", 1);
}
}
//return the first appearence once char in current stringstream
public char FirstAppearingOnce()
{
if(index==sb.length())return '#';
else {
return sb.charAt(index);
}
}
}

55 链表中环的入口节点

题目描述

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

思路
没想到什么更好的办法,就是没加一个暴力枚举它的下一个是否在前面。因为链表入口的确定需要比较的是地址而不是数值,判断相等只能用​​​==​​比较。而链表有环那么环一定在后半部分,本来next应该指向null的那个最后节点指向前面某个节点​​node​​.这个node就是入口节点。后面如果发现更好方法会补充。

实现代码

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

ListNode(int val) {
this.val = val;
}
}
*/
import java.util.ArrayList;
public class Solution {

public ListNode EntryNodeOfLoop(ListNode pHead)
{
ArrayList<ListNode>list=new ArrayList<ListNode>();
while (pHead.next!=null) {
for(int i=0;i<list.size();i++)
{
if(pHead.next==list.get(i))
{
return pHead.next;
}
}
ListNode pHead2=pHead;
list.add(pHead2);
pHead=pHead.next;
}
return null;
}
}

56 删除链表中重复节点

题目描述

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

思路
这题虽然很容易读懂题,但是处理起来如果选择方法不当可能还比较麻烦。因为是已经排好序的,只要是相同的那么节点值有相同的都要删除。但是删除时候你要考虑头节点问题。和尾部不能空指针异常(要判断是否删到头)。

对于头问题,如果头就有相等的那么这个头你就要处理一下,还有我们知道曾经链表我们引入一个带头节点的链表为了方便操作链表首。这里我们也可以这样操作。先用个node的next指向head,head先前移,最后返回head.next即可

有了这个头节点,就可以在不越界的情况下判断(node.next和node.next.next)的值是否等,如果等那么就往下删光所有值为它的节点。在这个过程要注意不要越界操作!!

剑指offer(51-59题)详解_算法_05


实现代码为:

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

ListNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public static ListNode deleteDuplication(ListNode pHead)
{
if(pHead==null)return null;
ListNode teamnode=new ListNode(Integer.MAX_VALUE);//当作头节点
teamnode.next=pHead;
pHead=teamnode;
while (teamnode.next!=null) {
System.out.println(teamnode.val);
while(teamnode.next.next!=null&&teamnode.next.val==teamnode.next.next.val)
{
int delete=teamnode.next.val;
while (teamnode.next!=null&&teamnode.next.val==delete) {
teamnode.next=teamnode.next.next;
}
if(teamnode.next==null)return pHead.next;
}
teamnode=teamnode.next;
}
return pHead.next;

//先处理一下
}
}

57 二叉树的下一个节点

题目描述

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

思路
这里他给的是一个节点(左右和父节点)。这题的处理方法虽然还是蛮多的,但是有的不一定好。关于​​​二叉树的几种遍历​​以前记过可以看看。

​比如​​你可以根据这个节点找到根节点吧?根节点知道的二叉树的非递归中序遍历前面讲过吧,根据顺序可以找到下一个。但是这样真的太低效了。

分析二叉树和分析这个节点的位置。 分类讨论就完全​​ojbk​​。

  • 右侧有儿子:这个节点只要有右侧节点那么它的下一个节点肯定在右侧节点,因为二叉树中序是左中右。只需要找到右侧最左的那个节点就ok。
  • 右侧无儿子找到上面第一个是父亲左节点的节点。因为这个节点没右儿子。就要看这个节点所在的这个子树的第一个中间节点了。就是​​[左侧区域最右]->中​​​的这个查找过程。当然,如果它的父节点祖先节点都不存在是左儿子的情况,那么它就是最右侧的节点,返回​​null​​就行了。
  • 剑指offer(51-59题)详解_数组_06

实现代码为:

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

TreeLinkNode(int val) {
this.val = val;
}
}
*/
public class Solution {
public TreeLinkNode GetNext(TreeLinkNode pNode)
{
if(pNode.right!=null)
{
pNode=pNode.right;
while (pNode.left!=null) {
pNode=pNode.left;
}
}
else if (pNode.next!=null&&pNode.next.left==pNode) {
pNode=pNode.next;
}
else {
while (pNode.next!=null&&pNode.next.right==pNode) {
pNode=pNode.next;
}
if(pNode.next!=null)pNode=pNode.next;
else {
pNode=null;
}
}
return pNode;
}
}

58 对称的二叉树

题目描述

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

思路
看看对不对称,照照镜子就行!看看镜子里你伸右手它是不是伸左手。而二叉树是不是对称的,你只需要根据某种遍历方式同时进行左右顺序颠倒的对比,查看节点的结构(有无左右孩子)和节点数值是否相等,如果有一点不一样直接停止返回即可!而这里笔者使用两个队列进行层序遍历一个规则是从左向右,另一个规则是从右向左 比较到最后就行了。

实现代码为:

import java.util.ArrayDeque;
import java.util.Queue;
/*
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;

public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
boolean isSymmetrical(TreeNode pRoot)
{
if(pRoot==null)return true;
Queue<TreeNode>q1=new ArrayDeque<>();//策略是左右
Queue<TreeNode>q2=new ArrayDeque<>();//策略是右左
q1.add(pRoot);
q2.add(pRoot);
while (!q1.isEmpty()&&!q2.isEmpty()) {
TreeNode t1=q1.poll();
TreeNode t2=q2.poll();
if(t1.val!=t2.val)return false;
if(!(t1.left==null)^(t2.right==null)&&!(t1.right==null)^(t2.left==null))//左右子树结构相同
{
if(t1.left!=null) {q1.add(t1.left);q2.add(t2.right);}
if(t1.right!=null) {q1.add(t1.right);q2.add(t2.left);}
}
else {
return false;
}
}
return true;

}
}

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

题目描述

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

思路
就是一个特殊的层序遍历。更换层的时候需要更换节点顺序,这需要我们用两个内存空间配合达到分清奇偶的目的。这里有的是从左到右,有的是从右到左,理论上​​可以借助栈​​将集合的元素反转但是没必要。我用两个List集合直接刚就行了。
首先进行分析:

  • 第一行从左到右,第二行从右到左,第三行从左到右。两个list装的是节点而还需要每次遍历根据奇数和偶数的特性将节点装起来
  • (普遍方法)你可以全部按照正常的顺序分层装起来,只不过如果偶数层遍历的时候从右往左加进结果集合。比较好想,容易操作,​​但是​​偶数层在添加节点时候不能同时遍历。
  • 但是笔者瞎搞发现一个规律。全部从右往左遍历。只不过在奇数行先添加(左后右)。而偶数行进行右左添加,相当于这个顺序操作一次被颠倒一次,每次添加节点都可以直接访问而不需要单独的访问。(​​这个方法可能复杂了上面一条其实就可以了​​)

实现代码(需要自己画图​​理解​​):

import java.util.ArrayList;

/*
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>>list=new ArrayList<ArrayList<Integer>>();
if(pRoot==null)return list;
ArrayList<TreeNode>nodelist1=new ArrayList<TreeNode>();//用来模拟堆栈用
ArrayList<TreeNode>nodelist2=new ArrayList<TreeNode>();
nodelist1.add(pRoot);
int num=1;//做奇数偶数
while (!nodelist1.isEmpty()||!nodelist2.isEmpty()) {
ArrayList<Integer>team=new ArrayList<Integer>();
if(num%2==1) {
for(int i=nodelist1.size()-1;i>=0;i--)
{

TreeNode teamNode=nodelist1.get(i);
team.add(teamNode.val);
if(teamNode.left!=null)nodelist2.add(teamNode.left);
if(teamNode.right!=null)nodelist2.add(teamNode.right);
}
nodelist1.clear();
}
else {
for(int i=nodelist2.size()-1;i>=0;i--)
{

TreeNode teamNode=nodelist2.get(i);
team.add(teamNode.val);
if(teamNode.right!=null)nodelist1.add(teamNode.right);
if(teamNode.left!=null)nodelist1.add(teamNode.left);

}
nodelist2.clear();
}
list.add(team);
num++;
}
return list;

}

}