文章目錄
歡迎關注個人數據結構專欄哈
劍指offer(1-10題)詳解
劍指offer(11-25題)詳解
劍指offer(26-33題)詳解
劍指offer(34-40題)詳解
劍指offer(41-50題)詳解
劍指offer(51-59題)詳解
劍指offer(60-67題)詳解
微信公衆號:bigsai
聲明:大部分題基本未參考題解,基本爲個人想法,如果由效率太低的或者錯誤還請指正!
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]
.當然,特殊情況和邊界需要特殊考慮下!
實現代碼爲:
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;
}
}
}
}
}
}
參考討論區
討論區的比薩大叔講的挺好的,另外就是我這裏事用數組拷貝來比較,而他用數組指標代替其實效率更高的。空間可以重複利用,並且數組複製佔用太多空間還是遞歸的!!可以參考下!
鏈接: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;
}
參考評論區:
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)的值是否等,如果等那麼就往下刪光所有值爲它的節點。在這個過程要注意不要越界操作!!
實現代碼爲:
/*
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
就行了。
實現代碼爲:
/*
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;
}
}