剑指offer和LeetCode题目笔记
目錄
- 劍指offer
- 一、數組
- 1. 數組中重復的數字
- 2.二維數組中的查找
- 多個if和if,elseif語句的區別
- 3.旋轉數組的最小數字
- 4.構建乘積數組
- 5.把數組排成最小的數
- 6.矩陣中的路徑
- 7.調整數組順序使奇數位于偶數前面
- 8.順時針打印矩陣
- 9.棧的壓入、彈出序列
- 10.數組中出現次數超過一半的數字
- 11.最小的k個數
- 13. 在排序數組中查找數字 I
- 14. 0~n-1中缺失的數字
- 15.數組中數字出現的次數
- 16. 和為s的兩個數字
- 17.撲克牌中的順子
- 18.數組中的逆序對
- 動態規劃
- 1.連續子數組的最大和
- 鏈表
- 反轉鏈表
- 二叉樹
- 二叉樹的鏡像
- 二叉搜索樹的第k大節點
- 二叉樹的最近公共祖先
劍指offer
一、數組
1. 數組中重復的數字
我的簡單思路:先排序,再輸出答案,但是效率不高。
下面是最好的思路:原地交換。數組元素的 索引 和 值 是 一對多 的關系。遍歷數組并通過交換操作,使元素的 索引 與 值 一一對應(即 nums[i] = inums[i]=i )。因而,就能通過索引映射對應的值,起到與字典等價的作用。
時間復雜度 O(N) : 遍歷數組使用 O(N)O(N) ,每輪遍歷的判斷和交換操作使用 O(1)O(1) 。
空間復雜度 O(1) : 使用常數復雜度的額外空間。
代碼:遍歷數組
1.所遍歷的數字=所在索引值,就遍歷下一個;
2.所遍歷的數字≠所在索引值,就和它該在的索引的數字進行交換;
3.所遍歷的數字=它該在的索引的數,則得出結果。
2.二維數組中的查找
思路:從右上角開始。對于每個元素,其左分支元素更小、右分支元素更大。
復雜度分析:
時間復雜度 O(M+N)O(M+N) :其中,NN 和 MM 分別為矩陣行數和列數,此算法最多循環 M+NM+N 次。
空間復雜度 O(1)O(1) : i, j 指針使用常數大小額外空間。
多個if和if,elseif語句的區別
①if無論是否滿足條件都會向下執行,知道程序結束,else if 滿足一個條件就會停止執行。
②由于if都會執行一遍,則可能會同一個需要判斷的事件,會進入2個if語句中,出現錯誤,而else if就不會發生這樣的事情。
3.旋轉數組的最小數字
思路:普通的太簡單了,使用二分查找。注意性質(下圖一目了然)。
循環二分: 設 m = (i + j) / 2m=(i+j)/2 為每次二分的中點( “/” 代表向下取整除法,因此恒有 i≤m<j ),可分為以下三種情況:
①當 nums[m] > nums[j]時: m一定在 左半,最小值一定在 [m+1,j] 閉區間內,因此執行 i = m + 1;
②當 nums[m] < nums[j] 時: m 一定在 右半,最小值 一定在[i, m]閉區間內,因此執行 j = m;
③當 nums[m] = nums[j] 時: 無法判斷 m 在哪個排序數組中,即無法判斷旋轉點 x 在 [i, m還是 [m + 1, j]區間中。解決方案: 執行 j = j - 1縮小判斷范圍
補充思考: 為什么本題二分法不用 nums[m]和 nums[i]作比較?
二分目的是判斷 m 在哪個排序數組中,從而縮小區間。而在 nums[m] > nums[i]情況下,無法判斷 m 在哪個排序數組中。本質上是由于 j 初始值肯定在右排序數組中; i 初始值無法確定在哪個排序數組中。舉例如下:
對于以下兩示例,當 i = 0, j = 4, m = 2時,有 nums[m] > nums[i] ,而結果不同。
[1, 2, 3, 4 ,5]旋轉點 x = 0 : m 在右排序數組(此示例只有右排序數組);
[3, 4, 5, 1 ,2] 旋轉點 x = 3 : m 在左排序數組。
4.構建乘積數組
思路:把圖畫出來就知道了。
5.把數組排成最小的數
Arrays.sort(Object[] oj1,new SortComparator()):這種方式能夠對引用類型數組,按照Comparator中聲明的compare方法對對象數組進行排序.
lamda表達式
compareTo()方法
增強for循環
相對于for(;;)而言 增強for循環有兩個好處:
1.寫起來簡單
2.遍歷集合、容器簡單
6.矩陣中的路徑
class Solution {public boolean exist(char[][] board, String word) {char[] words = word.toCharArray();//String轉換成char數組// 遍歷圖for(int i = 0; i < board.length; i++) {for(int j = 0; j < board[0].length; j++) {// 如果找到了,就返回true。否則繼續找if(dfs(board, words, i, j, 0)) {return true;}}}// 遍歷結束沒找到falsereturn false;}boolean dfs(char[][] board, char[] word, int i, int j, int k) {// 判斷傳入參數的可行性: i 與圖行數row比較,j與圖列數col比較,i,j初始都是0,都在圖左上角// k是傳入字符串當前索引,一開始是0,如果當前字符串索引和圖當前索引對應的值不相等,表示第一個數就不相等// 所以繼續找第一個相等的數。題目說第一個數位置不固定,即路徑起點不固定(不一定是左上角為第一個數)// 如果board[i][j] == word[k],則表明當前找到了對應的數,就繼續執行(標記找過,繼續dfs 上下右左) //board[i][j] != word[k]要放里面否則會有越界報錯if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]){return false;} // 表示每個字符都找到了// 一開始k=0,而word.length肯定不是0,所以沒找到,就執行dfs繼續找。if(k == word.length - 1) return true; // 訪問過的暫時標記空字符串:不標記會導致搜回去,//“ ”是空格 '\0'是空字符串,不一樣的!board[i][j] = '\0';// 順序是 上下 右 左(這四個順序順便);上面找到了對應索引的值所以k+1boolean res = dfs(board, word, i-1, j, k + 1) || dfs(board, word, i +1, j, k + 1) || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);// 還原找過的元素,因為之后可能還要訪問它的其他路徑board[i][j] = word[k];// 返回結果,如果false,則if(dfs(board, words, i, j, 0)) return true;不會執行,就會繼續找return res;} }7.調整數組順序使奇數位于偶數前面
考慮定義雙指針 i , j 分列數組左右兩端,循環執行:
指針 i 從左向右尋找偶數;
指針 j 從右向左尋找奇數;
將 偶數nums[i] 和 奇數 nums[j] 交換。
可始終保證: 指針 i 左邊都是奇數,指針 j 右邊都是偶數 。
8.順時針打印矩陣
思路:四個邊界,注意最后一個元素可能會重復讀取
9.棧的壓入、彈出序列
class Solution {public boolean validateStackSequences(int[] pushed, int[] popped) {Stack<Integer> stack = new Stack<>();int j=0;for(int i=0;i<pushed.length;i++){stack.push(pushed[i]);// 進棧while(!stack.isEmpty()&&stack.peek()==popped[j]){// 棧頂元素和出棧的元素相等就出棧,不相等繼續進棧stack.pop();j++;//出棧就往后遍歷poped}}return stack.isEmpty();} }10.數組中出現次數超過一半的數字
核心就是對拼消耗。
假設有一個擂臺,有一組人,每個人有編號,相同編號為一組,依次上場,沒人時上去的便是擂主(x),若有人,編號相同則繼續站著(人數+1),若不同,假設每個人戰斗力相同,都同歸于盡,則人數-1;那么到最后站著的肯定是人數占絕對優勢的那一組啦~
class Solution {public int majorityElement(int[] nums) {int persons=0;int leizhu=0;for(int i=0;i<nums.length;i++){if(persons==0){leizhu=nums[i];}persons=persons+(leizhu==nums[i]?1:-1);}return leizhu;} }11.最小的k個數
思路:題目只要求返回最小的 k 個數,對這 k 個數的順序并沒有要求。因此,只需要將數組劃分為 最小的 k 個數 和 其他數字 兩部分即可,而快速排序的哨兵劃分可完成此目標。
根據快速排序原理,如果某次哨兵劃分后 基準數正好是第 k+1 小的數字 ,那么此時基準數左邊的所有數字便是題目所求的 最小的 k 個數 。
根據此思路,考慮在每次哨兵劃分后,判斷基準數在數組中的索引是否等于 k ,若 true 則直接返回此時數組的前 k 個數字即可。
《啊哈!算法》 關于快速排序法為什么一定要哨兵j 先出動的原因?
假如i先動,如果后面沒有大于基準的數就導致排序錯誤!!
13. 在排序數組中查找數字 I
思路:二分查找找右邊界,簡單來說就是找到第一個比target大的數的位置。
假如tar-1不存在呢?比如{1,2,3,5,7,7,7,8},tar=7,而tar-1=6并不在數組nums里邊?;乜吹诙€問題if(nums[m] <= tar)合并了兩個條件, 上邊說了nums[m] = tar,這里說說nums[m] < tar。其實tar-1存在與否并不重要,按照開頭的數組,設tar=6。
第一次i = 0,j = 7,m=3,nums[m]=5<tar;
第二次 i = 4,j = 7,m = 5,nums[m] = 7 > tar;
第三次i = 4,j = 4,m = 4,nums[m] = 7 >tar;
第四次 i = 4,j = 3,i>j 退出while,renturn 4,tar的右邊界為下標4。
為什么tar不存在不影響?因為helper方法是為了求右邊界(簡單來說就是找到第一個比target大的數的位置),tar不存在時,不考慮nums[m] == tar的判斷;nums[m] > tar,j = m-1;nums[m] < tar,i =m+1,最終返回的是tar的右邊界(return i 保證是返回右邊界,不可用return j)。所以tar的右邊界一定存在,即使tar不存在。
class Solution {public int search(int[] nums, int target) {//helper(nums, target):找到target的右邊界//helper(nums, target - 1):找到target-1的右邊界return helper(nums, target) - helper(nums, target - 1);//target-1不存在也可以找到重復數字的左邊界}int helper(int[] nums, int tar) {int i = 0, j = nums.length - 1;while(i <= j) {int m = (i + j) / 2;if(nums[m] <= tar){//找重復數字的右邊界i = m + 1;}if(nums[m] > tar){j = m - 1;//中間值大于目標值,就在左半找} }return i;} }14. 0~n-1中缺失的數字
思路:
返回值: 跳出時,變量 i 和 j 分別指向 “右子數組的首位元素” 和 “左子數組的末位元素” 。因此返回 i 即可。
class Solution {public int missingNumber(int[] nums) {int i = 0, j = nums.length - 1;while(i <= j) {int mid = (i + j) / 2;if(nums[mid] == mid){//是正常數就找右半i = mid + 1;}else{j = mid - 1;//不是正常數就找左半} }return i;} }15.數組中數字出現的次數
思路:回歸異或的本質,1^0 = 1, 1^1 = 0, 0^0 = 1。a^b的結果里,為1的位表明a與b在這一位上不相同。這個不相同很關鍵,不相同就意味著我們在結果里任選一位位1的位置i,所有數可以按照i位的取值(0,1)分成兩組,那么a與b必然不在同一組里。再對兩組分別累計異或。那么兩個異或結果就是a、b。
一個例子:5,3,2,3,4,5
1.遍歷異或:結果為2=0100異或4=0010,n=0110=6
2.使用&找出a與b在這一位上不相同,不相同就可以把a和b劃分開。先0110&0001=0,然后0110&0010=1,m=2
3,劃分成了[0010=2,0011=3],[0100=4,0101=5],兩個子數組異或結果為2,5
16. 和為s的兩個數字
思路:就是簡單的雙指針
class Solution {public int[] twoSum(int[] nums, int target) {int i = 0, j = nums.length - 1;while(i < j) {int s = nums[i] + nums[j];if(s < target) i++;else if(s > target) j--;else return new int[] { nums[i], nums[j] };}return new int[0];} }17.撲克牌中的順子
思路:為什么不直接寫成==4,因為可能是0,0,3, 4 ,5
18.數組中的逆序對
思路:歸并排序中分治計算。
雙指針,因為子數組已經排好了序,每當遇到 左子數組當前元素 > 右子數組當前元素 時,意味著
「左子數組當前元素 至 左子數組末尾元素」 與 「右子數組當前元素」 構成了幾個 「逆序對」 。
動態規劃
1.連續子數組的最大和
思路:以某個數作為結尾,意思就是這個數一定會加上去,那么要看的就是這個數前面的部分要不要加上去。大于零就加,小于零就舍棄。
1.狀態定義: 設動態規劃列表 dp ,dp[i] 代表以元素 nums[i]為結尾的連續子數組最大和。
為何定義最大和 dp[i] 中必須包含元素 nums[i]:保證 dp[i]遞推到 dp[i+1] 的正確性;如果不包含 nums[i],遞推時則不滿足題目的 連續子數組 要求。
轉移方程: 若 dp[i?1]≤0 ,說明 dp[i?1] 對dp[i] 產生負貢獻,即 dp[i-1] + nums[i] 還不如 nums[i] 本身大。
①當 dp[i - 1] > 0 時:執行 dp[i] = dp[i-1] + nums[i] ;
②當 dp[i?1]≤0 時:執行 dp[i]=nums[i] ;
初始狀態: dp[0]=nums[0],即以 nums[0] 結尾的連續子數組最大和為 nnums[0] 。
返回值: 返回 dp 列表中的最大值,代表全局最大值。
鏈表
反轉鏈表
題目:
答案:
解析:
①ListNode last = reverse(head.next);
這個 reverse(head.next) 執行完成后,整個鏈表就成了這樣:
②head.next.next = head;
③
head.next = null;
return last;
二叉樹
二叉樹的鏡像
題目:請完成一個函數,輸入一個二叉樹,該函數輸出它的鏡像。
答案:
遞歸解析:
- 終止條件: 當節點 root 為空時(即越過葉節點),則返回 null;
- 遞推工作:
初始化節點 tmp ,用于暫存 root 的左子節點;
開啟遞歸 右子節點 mirrorTree(root.right) ,并將返回值作為 root 的 左子節點 。
開啟遞歸 左子節點 mirrorTree(tmp),并將返回值作為 root 的 右子節點 。 - 返回值: 返回當前節點 root ;
當結點是葉子結點是就會return它自己。
復雜度分析:
時間復雜度 O(N): 其中 N 為二叉樹的節點數量,建立二叉樹鏡像需要遍歷樹的所有節點,占用O(N) 時間。
空間復雜度 O(N) : 最差情況下(當二叉樹退化為鏈表),遞歸時系統需使用 O(N)大小的??臻g。
代碼:
二叉搜索樹的第k大節點
題目:給定一棵二叉搜索樹,請找出其中第k大的節點。
答案:求 “二叉搜索樹第 k 大的節點” 可轉化為求 “此樹的中序遍歷倒序的第 k 個節點”。
關鍵還是用類變量來維護k和res。類變量(也叫靜態變量)是類中獨立于方法之外的變量,用static 修飾。
public static int count=0, res=0;//形參k不能隨著dfs的迭代而不斷變化,為了記錄迭代進程和結果,引入類變量count和res。public int kthLargest(TreeNode root, int k) {count=k;//利用形參值k對類變量count進行初始化dfs(root);//這里不要引入形參k,dfs中直接使用的是初始值為k的類變量countreturn res; }public void dfs(TreeNode root){if(root==null||count==0) return;//當root為空或者已經找到了res時,直接返回dfs(root.right);if(--count==0){//先--,再判斷,比如count=3:2,1,0即代表第三個了res = root.val;return;//這里的return可以避免之后的無效迭代dfs(root.left);}dfs(root.left); }錯誤做法:
public static int n;public int kthLargest(TreeNode root, int k) {if(root==null){return 0;}kthLargest(root.right,k);n=--k;//這樣做n的值一直不變,所以k要作為類變量if(n==0){res=root.val;}kthLargest(root.left,k);return res;}二叉樹的最近公共祖先
答案:
根據以上定義,若 root 是p,q 的 最近公共祖先 ,則只可能為以下情況之一:
- p 和 q 在 root 的子樹中,且分列 root 的 異側(即分別在左、右子樹中);
- p=root ,且q 在 root 的左或右子樹中;
- q=root ,且 p 在 root 的左或右子樹中;
總結
以上是生活随笔為你收集整理的剑指offer和LeetCode题目笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 算法小抄笔记
- 下一篇: 怎么高效刷LeetCode?