文章目錄
劍指Offer.13 機器人的運動範圍 中等
題目中有一個計算橫縱座標數位之和的操作,這不是題目的關鍵點,將這個計算數位之和的方法封裝起來不要干擾主要的解題邏輯。
public int sums(int x,int y){
int ans=0;
while (x != 0) {
ans+=x%10;
x/=10;
}
while (y != 0) {
ans+=y%10;
y/=10;
}
return ans;
}
思路一:深搜
想法比較直接,就是從起點開始用深搜的方式遍歷矩陣,控制深搜邊界的同時判斷當前訪問的位置數位和是否小於等於k,並且需要一個標記數組記錄每個位置的訪問情況,防止重複計算。
雖然題目說是上下左右都可以移動,但是我們從左上角作爲起點開始只能向右或者向下移動,如果發生向左或者向上那一定是重複的搜索。
int m,n,k;
public int movingCount(int m, int n, int k) {
this.m=m;
this.n=n;
this.k=k;
//標記訪問過的位置
boolean[][] visited=new boolean[m][n];
return dfs(0,0,0,visited);
}
/**
* 深搜
* @param i 橫座標
* @param j 縱座標
* @param sum 座標數位和
* @param visited 標記數組
* @return
*/
private int dfs(int i, int j, int sum, boolean[][] visited) {
//如果 座標越界 或者 數位和大於k 或者 已經訪問過,則停止當前方向的深搜
if (i==m||j==n||sum>k||visited[i][j])return 0;
//標記爲已訪問
visited[i][j]=true;
//向下或者向右深搜
return 1+dfs(i+1,j,sums(i+1,j),visited)+dfs(i,j+1,sums(i,j+1),visited);
}
//計算數位和
public int sums(int x,int y){
int ans=0;
while (x != 0) {
ans+=x%10;
x/=10;
}
while (y != 0) {
ans+=y%10;
y/=10;
}
return ans;
}
時間複雜度:O(mn) 最壞情況,遍歷矩陣。
空間複雜度:O(mn)
思路二:廣搜
其實和深搜的思路一樣,只是換了一個搜索的方式,採用廣搜的方式尋找符合要求的位置。
//時間複雜度:O(mn)
public int movingCount(int m, int n, int k) {
//隊列保存座標
Queue<int[]> queue=new ArrayDeque<>();
//標記數組
boolean[][] visited=new boolean[m][n];
//廣搜
queue.add(new int[]{0,0});
int count=0;
visited[0][0]=true;
while (!queue.isEmpty()) {
int[] poll = queue.poll();
count++;
//向下、向右尋找符合要求的位置入隊並標記訪問狀態
//不越界 並且 數位和小於等於k 並且 未訪問過
if (poll[0] + 1 < m
&& sums(poll[0] + 1, poll[1]) <= k
&&!visited[poll[0]+1][poll[1]]){
queue.add(new int[]{poll[0]+1,poll[1]});
visited[poll[0]+1][poll[1]]=true;
}
if (poll[1] + 1 < n
&& sums(poll[0], poll[1] + 1) <= k
&&!visited[poll[0]][poll[1] + 1]){
queue.add(new int[]{poll[0],poll[1]+1});
visited[poll[0]][poll[1]+1]=true;
}
}
return count;
}
//計算數位和
public int sums(int x,int y){
int ans=0;
while (x != 0) {
ans+=x%10;
x/=10;
}
while (y != 0) {
ans+=y%10;
y/=10;
}
return ans;
}
時間複雜度:O(mn) 最壞情況,遍歷矩陣。
空間複雜度:O(mn)
劍指Offer.14_I 剪繩子 中等
思路一:動態規劃 dp[i]數組的含義:長度爲 i 的繩子剪斷後可以得到的最大乘積。
初始化:dp[1]=dp[2]=1,即長度爲 1 和 2 的繩子最大乘積是 1 。
狀態轉移:dp[i] = Max(dp[i],Max((i-j)*j,dp[i-j]*j))
,遍歷 [3,n] 長度的繩子,每種繩子都有j∈[0,i) 個位置可以進行剪斷,剪斷分爲兩種方式:1. 只在 j 位置剪斷一次,得到乘積 (i-j)*j
。2. 將剪斷後的部分 (i-j) 繼續剪斷,並選擇可以形成最大的乘積方法 dp[i-j] ,所以是 dp[i-j]*j
。從兩種方式裏選最大的。
//時間複雜度:O(n^2) 空間複雜度:O(n)
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
//初始化
dp[1]=dp[2]=1;
//[3,n] 種不通長度的繩子
for (int i = 3; i < n + 1; i++) {
//每個繩子有 [0,i) 個可剪斷的位置
for (int j = 0; j < i; j++) {
dp[i] = Math.max(dp[i],Math.max((i-j)*j,j*dp[i-j]));
}
}
return dp[n];
}
優化動態規劃 學習自 @Krahets 的題解中(我並沒有購買這本神書)。
爲使乘積最大,只有長度爲 2 和 3 的繩子不應再切分,且 3 比 2 更優 (詳情見下表)
//時間複雜度:O(n) 空間複雜度:O(n)
public int cuttingRope(int n) {
if (n < 4) return n - 1;
int[] dp = new int[n + 1];
dp[2] = 2;
dp[3] = 3;
for (int i = 4; i <= n; i++) {
dp[i] = Math.max(2 * dp[i - 2], 3 * dp[i - 3]);
}
return dp[n];
}
劍指Offer.14_II 剪繩子II 中等
(一個更簡單的辦法,直接使用 BigDecimal 大數類型,方法不變和上一題一樣。)
上題的優化解法,已經基本給出了本題的思路。本題的變化在於 n 的取值更大,可能出現整形溢出的情況。
思路一:貪心算法 我們首先考慮對於一段長n的繩子,我們可以切出的結果包含什麼?
1 會包含嗎? 不會,因爲 1 * (k - 1) < k , 只要把 1 和任何一個其他的片段組合在一起就有個更大的值
2 可以
3 可以
4 可以嗎? 它拆成兩個 2 的效果和本身一樣,因此也不考慮
5 以上可以嗎? 不可以,這些繩子必須拆,因爲總有一種拆法比不拆更優,比如拆成 k / 2 和 k - k / 2
綜上, 最後的結果只包含 2 和 3 (當然當總長度爲 2 和 3 時單獨處理), 那麼很顯然 n >= 5 時, 3*(n - 3) >= 2 * (n - 2) ,因此我們優先拆成 3 ,最後剩餘的拆成 2 。最後的結果一定是由若干個 3 和 1 或 2 個 2 組成。
//時間複雜度:O(logn) 空間複雜度:O(1)
public int cuttingRope(int n) {
if (n<4) return n-1;
long res = 1;
while (n > 4) {
res *= 3;
res %= 1000000007;
n -= 3;
}
//別忘了最後一段繩子,長度 n
return (int) (res*n%1000000007);
}
劍指Offer.15 二進制中 1 的個數 簡單
思路一:逐位統計 用 & 運算得到最後一位是 1 還是 0,每次統計完將 n 用 >>> 無符號右移 1 位。
public int hammingWeight(int n) {
int count = 0;
for (int i = 0; i < 32; i++) {
count += n & 1;
n >>>= 1;
}
return count;
}
思路二:減 1 統計 看例子:
n = 11010001
n = (n - 1)&n = 11010000 & 11010001 = 11010000
n = (n - 1)&n = 11001110 & 11010000 = 11000000
n = (n - 1)&n = 10111111 & 11000000 = 10000000
n = (n - 1)&n = 01111111 & 10000000 = 00000000 = 0
共計算了 4 次,得到結果:有 4 個 1 。
實現這個方式,每次計算之後計數器 +1 ,直至 n 變爲 0,則統計完成。
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
count++;
n &= (n - 1);
}
return count;
}
劍指Offer.16 數值的整數次方 中等
思路一:暴力法 這道題暴力法是不能通過leetcode判題機,會得到一個t。但是方法本身是可以得到正確答案的,所以我們需要對他進行優化。暴力法的想法很簡單的:2^3=2*2*2。
如果n爲負,則n=-n同時x=1/x,例如2^(-3)=1/2*1/2*1/2。但是這裏要注意n的取值範圍,主要是 正整數和負整數的不同範圍限制 。
public double myPow(double x, int n) {
if (x==0)return 0;
if (n==0)return 1;
double ans=1;
long N=n;
if (N<0){
N=-N;
x=1/x;
}
for (int i=0;i<N;i++){
ans*=x;
}
return ans;
}
時間複雜度:O(n)
思路二:二分法 當我們得到x^(n/2)的時候,我們不需要再去乘上n/2個x了,而是x^(n/2)*x^(n/2)=x^n。
這個想法用遞歸很容易實現,但是需要注意的是n的奇偶性,如果n爲奇數則需要再乘上一個x。
public double myPow(double x, int n) {
switch (n){
case 1:return x;
case 0:return 1;
case -1:return 1/x;
}
double half=myPow(x,n/2);
//奇偶性處理
double rest=myPow(x,n%2);
return half*half*rest;
}
時間複雜度:O(logn)
劍指Offer.17 打印從 1 到最大的 n 位數 簡單
思路一:冪運算 上一題剛搞過冪運算,直接拿來用。n 位數的需要申請 10^n-1
大小的數組。
//時間複雜度:O(10^n)
public int[] printNumbers(int n) {
int size = myPow(10, n);
int[] res = new int[size - 1];
//打印從 1 到 10^n-1
for (int i = 0; i < res.length; i++) {
res[i] = i + 1;
}
return res;
}
private int myPow(int x, int n) {
switch (n) {
case 1:
return x;
case 0:
return 1;
case -1:
return 1 / x;
}
int half = myPow(x, n / 2);
int rest = myPow(x, n % 2);
return half * half * rest;
}
劍指Offer.18 刪除鏈表的節點 簡單
思路一:遍歷 cur 指針遍歷鏈表,同時用一個 pre 指針指向 cur 節點的前驅, cur 指針找到 val 節點之後,刪除 cur 節點即可。
//時間複雜度:O(n)
public ListNode deleteNode(ListNode head, int val) {
if (head == null) return head;
if (head.val == val) return head.next;
ListNode cur = head.next, pre = head;
while (cur != null) {
if (cur.val != val) {
cur = cur.next;
pre = pre.next;
} else {
pre.next = cur.next;
break;
}
}
return head;
}
劍指Offer.19 正則表達式匹配 困難
思路一:回溯法 這種匹配思路其實就是不斷地減掉s和p的可以匹配首部,直至一個或兩個字符串被減爲空的時候,根據最終情況來得出結論。
如果只是兩個普通字符串進行匹配,按序遍歷比較即可:
if( s.charAt(i) == p.charAt(i) )
如果正則表達式字符串p只有一種"."一種特殊標記,依然是按序遍歷比較即可 :
if( s.charAt(i) == p.charAt(i) || p.charAt(i) == '.' )
上述兩種情況實現時還需要判斷字符串長度和字符串判空的操作。
但是,"*"這個特殊字符需要特殊處理,當p的第i個元素的下一個元素是星號時會有兩種情況:
- i元素需要出現0次,我們就保持s不變,將p的減掉兩個元素,調用isMatch。例如s:bc、p:a*bc,我們就保持s不變,減掉p的"a*",調用isMatch(s:bc,p:bc)。
- i元素需要出現一次或更多次,先比較i元素和s首元素,相等則保持p不變,s減掉首元素,調用isMatch。例如s:aabb、p:a*bb,就保持p不變,減掉s的首元素,調用isMatch(s:abb,p:a*bb)。
此時存在一些需要思考的情況,例如s:abb、p:a*abb,會用兩種方式處理:
- 按照上述第二種情況比較i元素和s首元素,發現相等就會減掉s的首字符,調用isMatch(s:bb,p:a*abb)。在按照上述第一種情況減去p的兩個元素,調用isMatch(s:bb,p:abb),最終導致false。
- 直接按照上述第一種情況減去p的兩個元素,調用isMatch(s:abb,p:abb),最終導致true。
所以說這算是一種暴力方法,會將所有的情況走一邊,看看是否存在可以匹配的情況。
public boolean isMatch(String s, String p) {
//如果正則串p爲空字符串s也爲空這匹配成功,如果正則串p爲空但是s不是空則說明匹配失敗
if (p.isEmpty())return s.isEmpty();
//判斷s和p的首字符是否匹配,注意要先判斷s不爲空
boolean headMatched=!s.isEmpty()&&(s.charAt(0)==p.charAt(0)||p.charAt(0)=='.');
if (p.length()>=2&&p.charAt(1)=='*'){//如果p的第一個元素的下一個元素是*
//則分別對兩種情況進行判斷
return isMatch(s,p.substring(2))||
(headMatched&&isMatch(s.substring(1),p));
}else if (headMatched){//否則,如果s和p的首字符相等
return isMatch(s.substring(1),p.substring(1));
}else {
return false;
}
}
時間複雜度:O((n+m)*2^(n+m/2)) n和m分別是s和p的長度
思路二:動態規劃法 本題的dp數組的含義就是:dp[i][j]就是s的前i個元素是否可以被p的前j個元素所匹配。
我們知道了dp數組的含義之後就知道了dp數組的幾個細節:
- dp[0][0]一定是true,因爲s爲空且p也爲空的時候一定是匹配的;dp[1][0]一定是false,因爲s有一個字符但是p爲空的時候一定是不匹配的。
- 這個boolean類型的dp數組的大小應該是dp[s.length+1][p.length+1],因爲我們不僅僅要分別取出s和p的所有元素,還要表示分別取s和p的0個元素時候(都爲空)的情況。
- 當寫到dp[s.length][p.length]的時候,我們就得到了最終s和p的匹配情況。
- dp[1][0]~dp[s.length][0]這一列都是false,因爲s不爲空但是p爲空一定不能匹配。
所以創建好dp數組之後,初始化dp[0][0]=true、dp[0][1]=false、dp[1][0]~dp[s.length][0]都是false。然後將第一行即dp[0][2]到dp[0][p.length]的元素初始化。
第一行初始化思路:如果不爲空的p想要匹配上爲空的s,因爲此時p已經不爲空,則需要p是"a*"、“b*”、“c*”。。。這種形式的才能匹配上。
然後填寫數組的其餘部分,這個過程中如果p.charAt(j)==’*'依然是遵循上題中的兩種情況;否則就判斷兩個字符串的i和j號字符是否相等,相等則分別減除當前字符繼續判斷,不相等則直接等於false。
public boolean isMatch(String s, String p) {
//需要分別取出s和p爲空的情況,所以dp數組大小+1
boolean[][] dp=new boolean[s.length()+1][p.length()+1];
//初始化dp[0][0]=true,dp[0][1]和dp[1][0]~dp[s.length][0]默認值爲false所以不需要顯式初始化
dp[0][0]=true;
//填寫第一行dp[0][2]~dp[0][p.length]
for (int k=2;k<=p.length();k++){
//p字符串的第2個字符是否等於'*',此時j元素需要0個,所以s不變p減除兩個字符
dp[0][k]=p.charAt(k-1)=='*'&&dp[0][k-2];
}
//填寫dp數組剩餘部分
for (int i=0;i<s.length();i++){
for (int j=0;j<p.length();j++){
//p第j個字符是否爲*
if (p.charAt(j)=='*'){
//兩種情況:1.s不變[i+1],p移除兩個元素[j+1-2]。
// 2.比較s的i元素和p的j-1(因爲此時j元素爲*)元素,相等則移除首元素[i+1-1],p不變。
dp[i+1][j+1]=dp[i+1][j-1]||
(dp[i][j+1]&&headMatched(s,p,i,j-1));
}else {
//s的i元素和p的j元素是否相等,相等則移除s的i元素[i+1-1]和p的j元素[j+1-1]
dp[i+1][j+1]=dp[i][j]&&headMatched(s,p,i,j);
}
}
}
return dp[s.length()][p.length()];
}
//判斷s第i個字符和p第j個字符是否匹配
public boolean headMatched(String s,String p,int i,int j){
return s.charAt(i)==p.charAt(j)||p.charAt(j)=='.';
}
時間複雜度:O(n*m) n和m分別是s和p的長度
有了第一題總結的"經驗"之後,這道題邏輯上不難理解,但是細節上尤其各種下標值非常的噁心。
劍指Offer.20 表示數值的字符串 中等
礙於篇幅,就贅述這種噁心人的題目了!
之前寫過一篇博文專門記錄過這道題:[LeetCode No.65]——什麼是面向測試編程?看看本這道題就知道了!口區
劍指Offer.21 調整數組順序使奇數位於偶數前面 簡單
思路一:雙指針,快排變形 是 LeetCode NO.75 顏色分類 中變形三路快排思路的簡化版。雙指針,分別找順序第一個偶數,和逆序第一個奇數,然後交換,一次遍歷即可完成交換。
//時間複雜度:O(n)
public int[] exchange(int[] nums) {
int i = 0, j = nums.length - 1;
while (i < j) {
//順序找第一個偶數
while (i<j&&(nums[i]&1)==1)i++;
//逆序找第一個奇數
while (i<j&&(nums[j]&1)==0)j--;
//交換位置
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
return nums;
}
劍指Offer.22 鏈表中倒數第 k 個節點 簡單
- 用兩個指針 slow、fast 分別指向鏈表的開頭(啞節點)。
- 先讓 fast 指針逐步移動到距離 slow 指針 k 的位置上,也就是上 slow 指針和 fast 指針 n 個間隔。
- 讓 slow 指針和 fast 指針同時向後移動,直至 fast 指針爲 null。
- 此時 slow 指針指向的就是倒數第 k 個節點。
//時間複雜度:O(n)
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head, slow = head;
for (int i = 1; i <= k; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
劍指Offer.24 翻轉鏈表 簡單
思路一:三指針迭代 curr 指向當前待翻轉的節點、pre 指向前驅、next 記錄後繼。
//時間複雜度:O(N)
public ListNode reverseList(ListNode head) {
ListNode curr = head, pre = null;
while (curr != null) {
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
劍指Offer.25 合併兩個排序的鏈表 簡單
思路一:遍歷 遍歷比較每個節點,根據節點大小,連接成一個新鏈表。最後不要忘了,較長鏈表的剩餘部分直接拼接在新鏈表尾部。
//時間複雜度:O(n) 較短的那個鏈表長度爲 n
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
p.next = l1;
l1 = l1.next;
} else {
p.next = l2;
l2 = l2.next;
}
p = p.next;
}
p.next = l1 != null ? l1 : l2;
return dummy.next;
}
本人菜鳥,有錯誤請告知,感激不盡!