演算法--陣列、連結串列、棧、佇列
一、陣列
1、刪除有序陣列中的重複項(簡單)
題目地址:https://leetcode.cn/problems/remove-duplicates-from-sorted-array/
給你一個 升序排列 的陣列 nums
,請你 原地 刪除重複出現的元素,使每個元素 只出現一次 ,返回刪除後陣列的新長度。元素的 相對順序 應該保持 一致 。
由於在某些語言中不能改變陣列的長度,所以必須將結果放在陣列nums的第一部分。更規範地說,如果在刪除重複項之後有 k
個元素,那麼 nums
的前 k
個元素應該儲存最終結果。
將最終結果插入 nums
的前 k
個位置後返回 k
。
不要使用額外的空間,你必須在
判題標準:
系統會用下面的程式碼來測試你的題解:
int[] nums = [...]; // 輸入陣列
int[] expectedNums = [...]; // 長度正確的期望答案
int k = removeDuplicates(nums); // 呼叫
assert k == expectedNums.length;
for (int i = 0; i < k; i++) {
assert nums[i] == expectedNums[i];
}
如果所有斷言都通過,那麼您的題解將被 通過。
示例 1:
輸入:nums = [1,1,2] 輸出:2, nums = [1,2,_] 解釋:函式應該返回新的長度 2 ,並且原陣列 nums 的前兩個元素被修改為 1, 2 。不需要考慮陣列中超出新長度後面的元素。
示例 2:
輸入:nums = [0,0,1,1,1,2,2,3,3,4]
輸出:5, nums = [0,1,2,3,4]
解釋:函式應該返回新的長度 5 , 並且原陣列 nums 的前五個元素被修改為 0, 1, 2, 3, 4 。不需要考慮陣列中超出新長度後面的元素。
提示:
-
1 <= nums.length <= 3 * 104
-
-104 <= nums[i] <= 104
-
nums
已按 升序 排列
解答
主要使用雙指標
class Solution { public int removeDuplicates(int[] nums) { if (nums == null || nums.length == 0 || nums.length==1){ return nums.length; } int j = 0; for(int i =1; i< nums.length; i++){ if(nums[i] != nums[j]){ nums[++j] = nums[i]; } } return j+1; } }
2、移動零(簡單)
題目地址:https://leetcode.cn/problems/move-zeroes/
給定一個數組 nums
,編寫一個函式將所有 0
移動到陣列的末尾,同時保持非零元素的相對順序。
請注意 ,必須在不復制陣列的情況下原地對陣列進行操作。
示例 1:
輸入: nums = [0,1,0,3,12]
輸出: [1,3,12,0,0]
示例 2:
輸入: nums = [0]
輸出: [0]
提示:
1 <= nums.length <= 104
-231 <= nums[i] <= 231 - 1
解答
仍然使用雙指標即可
class Solution {
public void moveZeroes(int[] nums) {
int j = 0;
for(int i=0; i<nums.length; i++){
if(nums[i] != 0){
nums[j] = nums[i];
j++;
}
}
while(j < nums.length){
nums[j++] = 0;
}
}
}
3、合併兩個有序陣列(簡單)
題目地址:https://leetcode.cn/problems/merge-sorted-array/
給你兩個按 非遞減順序 排列的整數陣列 nums1
和 nums2
,另有兩個整數 m
和 n
,分別表示 nums1
和 nums2
中的元素數目。
請你 合併 nums2
到 nums1
中,使合併後的陣列同樣按 非遞減順序 排列。
注意:最終,合併後陣列不應由函式返回,而是儲存在陣列 nums1
中。為了應對這種情況,nums1
的初始長度為 m + n
,其中前 m
個元素表示應合併的元素,後 n
個元素為 0
,應忽略。nums2
的長度為 n
。
示例 1:
輸入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
輸出:[1,2,2,3,5,6]
解釋:需要合併 [1,2,3] 和 [2,5,6] 。
合併結果是 [1,2,2,3,5,6] ,其中斜體加粗標註的為 nums1 中的元素。
示例 2:
輸入:nums1 = [1], m = 1, nums2 = [], n = 0
輸出:[1]
解釋:需要合併 [1] 和 [] 。
合併結果是 [1] 。
示例 3:
輸入:nums1 = [0], m = 0, nums2 = [1], n = 1
輸出:[1]
解釋:需要合併的陣列是 [] 和 [1] 。
合併結果是 [1] 。
注意,因為 m = 0 ,所以 nums1 中沒有元素。nums1 中僅存的 0 僅僅是為了確保合併結果可以順利存放到 nums1 中。
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109
進階:你可以設計實現一個時間複雜度為 O(m + n)
的演算法解決此問題嗎?
解答
這道題仍然使用雙指標,還有一個重要的點,就是從後往前迴圈,這樣不會涉及資料的移動,時間複雜度為O(n+m),如果從前往後迴圈,會存在單個數組中中間插入的情況,時間複雜度為O(n+logn*m)
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i = m-1, j = n-1, k = m+n-1;
while(j >= 0){
if(i < 0){
nums1[k--] = nums2[j--];
continue;
}
if(nums1[i] >= nums2[j]){
nums1[k--] = nums1[i--];
}else{
nums1[k--] = nums2[j--];
}
}
}
}
4、加一(簡單)
題目地址:https://leetcode.cn/problems/plus-one/description/
給定一個由 整數 組成的 非空 陣列所表示的非負整數,在該數的基礎上加一。
最高位數字存放在陣列的首位, 陣列中每個元素只儲存單個數字。
你可以假設除了整數 0 之外,這個整數不會以零開頭。
示例 1:
輸入:digits = [1,2,3]
輸出:[1,2,4]
解釋:輸入陣列表示數字 123。
示例 2:
輸入:digits = [4,3,2,1]
輸出:[4,3,2,2]
解釋:輸入陣列表示數字 4321。
示例 3:
輸入:digits = [0]
輸出:[1]
提示:
1 <= digits.length <= 100
0 <= digits[i] <= 9
解答
class Solution {
public int[] plusOne(int[] digits) {
int j = 0;
int k = 0;
for(int i=digits.length-1; i>= 0;i--){
k = i == digits.length-1 ? 1 : 0;
int sum = digits[i] + j + k;
if(sum == 10){
digits[i] = 0;
j = 1;
}else{
digits[i] = sum;
j = 0;
break;
}
}
if(j != 0){
digits = new int[digits.length+1];
digits[0] = 1;
}
return digits;
}
}
二、連結串列
1、反轉連結串列(簡單)
題目地址:https://leetcode.cn/problems/reverse-linked-list/
給你單鏈表的頭節點 head
,請你反轉連結串列,並返回反轉後的連結串列。
示例 1:
輸入:head = [1,2,3,4,5]
輸出:[5,4,3,2,1]
示例 2:
輸入:head = [1,2]
輸出:[2,1]
示例 3:
輸入:head = []
輸出:[]
提示:
- 連結串列中節點的數目範圍是
[0, 5000]
-5000 <= Node.val <= 5000
進階:連結串列可以選用迭代或遞迴方式完成反轉。你能否用兩種方法解決這道題?
解答
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null){
return head;
}
ListNode preNode = head;
ListNode node = head.next;
preNode.next = null;
while(node != null){
ListNode nextNode = node.next;
node.next = preNode;
preNode = node;
node = nextNode;
}
return preNode;
}
}
2、K 個一組翻轉連結串列(困難)
題目地址:https://leetcode.cn/problems/reverse-nodes-in-k-group/description/
給你連結串列的頭節點 head
,每 k
個節點一組進行翻轉,請你返回修改後的連結串列。
k
是一個正整數,它的值小於或等於連結串列的長度。如果節點總數不是 k
的整數倍,那麼請將最後剩餘的節點保持原有順序。
你不能只是單純的改變節點內部的值,而是需要實際進行節點交換。
示例 1:
輸入:head = [1,2,3,4,5], k = 2
輸出:[2,1,4,3,5]
示例 2:
輸入:head = [1,2,3,4,5], k = 3
輸出:[3,2,1,4,5]
提示:
- 連結串列中的節點數目為
n
1 <= k <= n <= 5000
0 <= Node.val <= 1000
進階:你可以設計一個只用 O(1)
額外記憶體空間的演算法解決此問題嗎?
解答
解題思路:
1、先分組
2、對於每一組進行翻轉
3、對組進行前後關係設定
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode protectNode = new ListNode(0, head);
ListNode preHead = protectNode;
while(head != null){
ListNode end = getEndNode(head, k);
if(end == null){
break;
}
ListNode nextHead = end.next;
reverse(head, nextHead);
preHead.next = end;
head.next = nextHead;
preHead = head;
head = nextHead;
}
return protectNode.next;
}
private ListNode getEndNode(ListNode head, int k){
int i=1;
while(i<k && head != null){
head = head.next;
i++;
}
return head;
}
private void reverse(ListNode head, ListNode end){
ListNode pre = null;
while(head != null && head != end){
ListNode temp = head.next;
head.next = pre;
pre = head;
head = temp;
}
}
}
3、環形連結串列(簡單)
題目地址:https://leetcode.cn/problems/linked-list-cycle/description/
給你一個連結串列的頭節點 head
,判斷連結串列中是否有環。
如果連結串列中有某個節點,可以通過連續跟蹤 next
指標再次到達,則連結串列中存在環。 為了表示給定連結串列中的環,評測系統內部使用整數 pos
來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。注意:pos
不作為引數進行傳遞 。僅僅是為了標識連結串列的實際情況。
如果連結串列中存在環 ,則返回 true
。 否則,返回 false
。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:連結串列中有一個環,其尾部連線到第二個節點。
示例 2:
輸入:head = [1,2], pos = 0
輸出:true
解釋:連結串列中有一個環,其尾部連線到第一個節點。
示例 3:
輸入:head = [1], pos = -1
輸出:false
解釋:連結串列中沒有環。
提示:
- 連結串列中節點的數目範圍是
[0, 104]
-105 <= Node.val <= 105
-
pos
為-1
或者連結串列中的一個 有效索引 。
進階:你能用 O(1)
(即,常量)記憶體解決此問題嗎?
解答
主要用到快慢指標,如果快慢指標走到同一個位置,說明有迴圈連結串列,否則就不存在
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
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;
}
}
4、環形連結串列2(中等)
題目地址:https://leetcode.cn/problems/linked-list-cycle-ii/description/
給定一個連結串列的頭節點 head
,返回連結串列開始入環的第一個節點。 如果連結串列無環,則返回 null
。
如果連結串列中有某個節點,可以通過連續跟蹤 next
指標再次到達,則連結串列中存在環。 為了表示給定連結串列中的環,評測系統內部使用整數 pos
來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。如果 pos
是 -1
,則在該連結串列中沒有環。注意:pos
不作為引數進行傳遞,僅僅是為了標識連結串列的實際情況。
不允許修改 連結串列。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:返回索引為 1 的連結串列節點
解釋:連結串列中有一個環,其尾部連線到第二個節點。
示例 2:
輸入:head = [1,2], pos = 0
輸出:返回索引為 0 的連結串列節點
解釋:連結串列中有一個環,其尾部連線到第一個節點。
示例 3:
輸入:head = [1], pos = -1
輸出:返回 null
解釋:連結串列中沒有環。
提示:
- 連結串列中節點的數目範圍在範圍
[0, 104]
內 -105 <= Node.val <= 105
-
pos
的值為-1
或者連結串列中的一個有效索引
進階:你是否可以使用 O(1)
空間解決此題?
解答
這道題主要是解題思路,程式碼並不難
首先定義幾個變數,x:表示環形連結串列前的長度,y:表示從環形連結串列開始到快慢指標相遇的長度,r:表示環形連結串列的長度
那麼,快指標走的長度為:fast = x + mr+y,滿指標走的長度為:slow= x + nr+y,其中m和n分別表示快慢指標迴圈環形連結串列的次數
由於快指標走的速度是滿指標的2倍,因此就有 fast = 2slow,移動後 x = (m-2n)r - y,也就是從環形連結串列相遇的節點開始到環形連結串列的開始節點的長度 與 連結串列頭結點到環形連結串列節點開始節點長度一致
基於以上分析,解題思路如下:
1、首先使用快慢指標找到相同節點
2、再用兩個指標,一個從環形連結串列相遇節點往後遞進,一個從連結串列頭結點向前遞進,最終匯合的節點就是環形連結串列的開始節點
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
ListNode meetNode = null;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
meetNode = fast;
break;
}
}
if(meetNode == null){
return null;
}
while(meetNode != head){
head = head.next;
meetNode = meetNode.next;
}
return meetNode;
}
}
5、合併兩個有序連結串列(簡單)
題目地址:https://leetcode.cn/problems/merge-two-sorted-lists/description/
將兩個升序連結串列合併為一個新的 升序 連結串列並返回。新連結串列是通過拼接給定的兩個連結串列的所有節點組成的。
示例 1:
輸入:l1 = [1,2,4], l2 = [1,3,4]
輸出:[1,1,2,3,4,4]
示例 2:
輸入:l1 = [], l2 = []
輸出:[]
示例 3:
輸入:l1 = [], l2 = [0]
輸出:[0]
提示:
- 兩個連結串列的節點數目範圍是
[0, 50]
-100 <= Node.val <= 100
-
l1
和l2
均按 非遞減順序 排列
解答:
比較簡單,就不做說明了,這裡說一個易錯點,while(list1 != null || list2 != null)
特別容易寫成while(list1 != null && list2 != null)
,需要注意
另外可以把 if(list1 == null){
與 if(list1.val <= list2.val)
合併到一個分支為 if(list2 == null || (list1 != null && list1.val <= list2.val))
,不過這樣如果 list2 為 null,仍然需要迴圈 list1 的長度,如果直接設定完 break,就不需要迴圈。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode protectNode = new ListNode();
ListNode head = protectNode;
while(list1 != null || list2 != null){
if(list1 == null){
head.next = list2;
break;
}
if(list2 == null){
head.next = list1;
break;
}
if(list1.val <= list2.val){
head.next = list1;
ListNode temp = list1.next;
list1.next = null;
list1 = temp;
}else{
head.next = list2;
ListNode temp = list2.next;
list2.next = null;
list2 = temp;
}
head = head.next;
}
return protectNode.next;
}
}
三、棧與佇列
0、概述
棧對先進後出,佇列時先進先出,比較簡單,沒什麼可說的
1、有效的括號(簡單)
題目地址:https://leetcode.cn/problems/valid-parentheses/
給定一個只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字串 s
,判斷字串是否有效。
有效字串需滿足:
- 左括號必須用相同型別的右括號閉合。
- 左括號必須以正確的順序閉合。
- 每個右括號都有一個對應的相同型別的左括號。
示例 1:
輸入:s = "()"
輸出:true
示例 2:
輸入:s = "()[]{}"
輸出:true
示例 3:
輸入:s = "(]"
輸出:false
提示:
1 <= s.length <= 104
-
s
僅由括號'()[]{}'
組成
解答
使用棧先進先出的特性
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack();
for(char c : s.toCharArray()){
if(c == '(' || c== '[' || c == '{'){
stack.push(c);
}else{
if(stack.isEmpty()){
return false;
}
char top = stack.pop();
if(c == ')' && '(' != top
|| c == ']' && '[' != top
|| c == '}' && '{' != top){
return false;
}
}
}
return stack.isEmpty();
}
}
2、最小棧(中等)
題目地址:https://leetcode.cn/problems/min-stack/description/
設計一個支援 push
,pop
,top
操作,並能在常數時間內檢索到最小元素的棧。
實現 MinStack
類:
-
MinStack()
初始化堆疊物件。 -
void push(int val)
將元素val推入堆疊。 -
void pop()
刪除堆疊頂部的元素。 -
int top()
獲取堆疊頂部的元素。 -
int getMin()
獲取堆疊中的最小元素。
示例 1:
輸入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
輸出:
[null,null,null,null,-3,null,0,-2]
解釋:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
提示:
-231 <= val <= 231 - 1
-
pop
、top
和getMin
操作總是在 非空棧 上呼叫 -
push
,pop
,top
, andgetMin
最多被呼叫3 * 104
次
解答
要求最小值,那麼就需要額外的空間進行儲存,就定義一個 minStack
最小值棧的儲存,每一次 push 時,都記錄當前的最小值,例如上面的 -2,0,-3 的順序
push -2 時,最小值棧為空,最小值為 -2
push 0 時,最小值為 -2
push -3 時,最小值為 -3
因此就可以定義一個最小值棧,每一次 push 時,儲存的是原棧中的最小值,那麼每一次 pop 原棧時,就需要把最小值棧也 pop 一次。
注:之前想過使用三個棧,原棧還存在,另外有一個最小值棧和一個臨時棧,臨時棧完全作為新來資料時,最小值棧為了重新排序而設定的一個臨時儲存空間。這樣思路也沒問題,但是當代碼提交時,提示超出了執行時間,看來這樣方式的效率還是不行。
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
public MinStack() {
stack = new Stack();
minStack = new Stack();
}
public void push(int val) {
stack.push(val);
val = minStack.isEmpty() ? val : Math.min(val, minStack.peek());
minStack.push(val);
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
3、逆波蘭表示式求值(中等)
題目地址:https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/
根據 逆波蘭表示法,求表示式的值。
有效的算符包括 +
、-
、*
、/
。每個運算物件可以是整數,也可以是另一個逆波蘭表示式。
注意 兩個整數之間的除法只保留整數部分。
可以保證給定的逆波蘭表示式總是有效的。換句話說,表示式總會得出有效數值且不存在除數為 0 的情況。
示例 1:
輸入:tokens = ["2","1","+","3","*"]
輸出:9
解釋:該算式轉化為常見的中綴算術表示式為:((2 + 1) * 3) = 9
示例 2:
輸入:tokens = ["4","13","5","/","+"]
輸出:6
解釋:該算式轉化為常見的中綴算術表示式為:(4 + (13 / 5)) = 6
示例 3:
輸入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
輸出:22
解釋:該算式轉化為常見的中綴算術表示式為:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
提示:
1 <= tokens.length <= 104
-
tokens[i]
是一個算符("+"
、"-"
、"*"
或"/"
),或是在範圍[-200, 200]
內的一個整數
逆波蘭表示式:
逆波蘭表示式是一種字尾表示式,所謂字尾就是指算符寫在後面。
- 平常使用的算式則是一種中綴表示式,如
( 1 + 2 ) * ( 3 + 4 )
。 - 該算式的逆波蘭表示式寫法為
( ( 1 2 + ) ( 3 4 + ) * )
。
逆波蘭表示式主要有以下兩個優點:
- 去掉括號後表示式無歧義,上式即便寫成
1 2 + 3 4 + *
也可以依據次序計算出正確結果。 - 適合用棧操作運算:遇到數字則入棧;遇到算符則取出棧頂兩個數字進行計算,並將結果壓入棧中
解答
用到了棧,如果不是運算子,就往棧裡新增,如果是運算子,就從棧裡面獲取兩個資料進行運算,再把運算結果放入棧中
易錯點:pop 的時候要注意兩個值的先後順序,第一個 pop 的是運算子後面的資料,第二個 pop 的資料是運算子前面的資料,對於減法和除法要特別注意。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> numStack = new Stack();
for(String s : tokens){
if(s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")){
int y = numStack.pop();
int x = numStack.pop();
if(s.equals("+")){
numStack.push(x + y);
}
if(s.equals("-")){
numStack.push(x - y);
}
if(s.equals("*")){
numStack.push(x * y);
}
if(s.equals("/")){
numStack.push(x / y);
}
}else{
numStack.push(Integer.parseInt(s));
}
}
return numStack.pop();
}
}
4、面試題 16.26. 計算器(中等)
題目地址:https://leetcode.cn/problems/calculator-lcci/description/
給定一個包含正整數、加(+)、減(-)、乘(*)、除(/)的算數表示式(括號除外),計算其結果。
表示式僅包含非負整數,+
, -
,*
,/
四種運算子和空格
。 整數除法僅保留整數部分。
示例 1:
輸入: "3+2*2"
輸出: 7
示例 2:
輸入: " 3/2 "
輸出: 1
示例 3:
輸入: " 3+5 / 2 "
輸出: 5
說明:
- 你可以假設所給定的表示式都是有效的。
- 請不要使用內建的庫函式
eval
。
解答
解題思路:
1、中綴轉字尾:可以將中綴表示式改為字尾表示式,即上面的逆波蘭表示式,就是將運算子放入兩個需要運算的資料之後,然後出一個運算子,就接著出兩個數字,運算完畢後將資料再讓進去。
2、定義兩個棧分別儲存數字和運算子:那麼為了更好的獲取數字和獲取運算子,就需要使用兩個棧,一個儲存資料,一個儲存運算子,即迴圈字元,如果是數字的就放入數字的棧,如果是運算子的就放入運算子的棧
3、運算子優先順序:由於運算子有優先順序,因此不能無腦的將數字和運算子放入棧中,如果新來的運算子優先順序小於等於棧中已有的優先順序,則先將棧中的運算子計算
4、最後出棧:在全部迴圈完畢後,有可能符號棧中還有運算子沒有出棧,因此需要再判斷將運算子棧中資料出棧完
5、結束控制:由於可能出現兩位及以上的數字,因此在判斷數字時,使用的是字元判斷,每次和之前的相加,直到遇到非數字才停止,並將該數字入棧,那麼就存在最後一個數字沒法入棧的情況;另外題目中提示可能有空格,那麼就取個巧,在字串的最後加上一個空格,保證最後一個數值可以入棧。
取巧點:字串後加空格保證最後一個數值入棧
易錯點:在判斷符號優先順序時,有兩個點容易出錯:
1、第一個是優先順序的判斷,必須是前面的優先順序大於等於當前準備入棧的優先順序,就需要先計算前面的結果,必須包含等於,因為不包含的話,就可能出現 1-1+1=-1
的情況(因為這兩個服務的優先順序一樣,沒有先做前面的運算,而都放入了棧中,最後使用字尾表示式,先算後面再算前面)
2、第二個 for 迴圈中的 while,這裡比較容易被誤用 if,如果使用 if,就可能出現 2-3/4+5=-3
的情況,因為 if 只判斷一次,那麼第一個減號,直接入棧,第二個除號,比之前的優先極高,也入棧,到了加號,比之前的優先順序低,那麼優先算 3/4
,但是由於 if 只判斷了一次,就會變為 2-0+5
,根據字尾表示式,就會產生 -3 的結果,使用 while 可以一直向前判斷,在 3/4
算完後,繼續使用加號和之前的運算子比較(減號),仍然是小於等於,那麼之前的繼續運算,最後就會得到 2+5
,最終值為7
class Solution {
public int calculate(String s) {
Stack<Integer> numStack = new Stack();
Stack<Character> opsStack = new Stack();
s += " ";
String numStr = "";
for(char ch : s.toCharArray()){
if(ch >= '0' && ch <= '9'){
numStr += ch;
continue;
}else if(!"".equals(numStr)){
numStack.push(Integer.parseInt(numStr));
numStr = "";
}
if(ch == ' '){
continue;
}
while(!opsStack.isEmpty() && getRange(opsStack.peek()) >= getRange(ch)){
numStack.push(grtResult(numStack.pop(), numStack.pop(), opsStack.pop()));
}
opsStack.push(ch);
}
while(!opsStack.isEmpty()){
numStack.push(grtResult(numStack.pop(), numStack.pop(), opsStack.pop()));
}
return numStack.pop();
}
private int getRange(char ch){
if(ch == '*' || ch == '/'){return 2;}
if(ch == '+' || ch == '-'){return 1;}
return 0;
}
private int grtResult(int val1, int val2, char ops){
if(ops == '+'){return val2 + val1;}
if(ops == '-'){return val2 - val1;}
if(ops == '*'){return val2 * val1;}
return val2 / val1;
}
}
5、基本計算器(困難)
題目地址:https://leetcode.cn/problems/basic-calculator/description/
給你一個字串表示式 s
,請你實現一個基本計算器來計算並返回它的值。
注意:不允許使用任何將字串作為數學表示式計算的內建函式,比如 eval()
。
示例 1:
輸入:s = "1 + 1"
輸出:2
示例 2:
輸入:s = " 2-1 + 2 "
輸出:3
示例 3:
輸入:s = "(1+(4+5+2)-3)+(6+8)"
輸出:23
提示:
1 <= s.length <= 3 * 105
-
s
由數字、'+'
、'-'
、'('
、')'
、和' '
組成 -
s
表示一個有效的表示式 - '+' 不能用作一元運算(例如, "+1" 和
"+(2 + 3)"
無效) - '-' 可以用作一元運算(即 "-1" 和
"-(2 + 3)"
是有效的) - 輸入中不存在兩個連續的操作符
- 每個數字和執行的計算將適合於一個有符號的 32位 整數
解答
這道題和上面題有兩個大的區別,一個是有括號,一個是可能存在負號,而上面的題有乘除,這道題沒有,則不用考慮,那麼直接把上面問題的解答拿下來,針對這兩個大的區別點做針對的修改。
1、有括號:主要新增對於括號的判斷,如果是左括號,直接放入符號棧中,如果是右括號,則直接向前進行處理,直到遇到一個左括號
2、負值:其實這道題出的有問題,並沒有描述存在負號的情況,但是在提交的時候會又能相關的測試用例,導致提交不成功。
對於負號的問題,可以通過在負號前補零來處理,那麼就要看哪些場景需要補零了。
補零的場景:如果負號在左括號後面,或者在加減號後面,都是需要補零的,那麼就可以設定一個標誌,在這兩個場景時,將該標誌設定為true,但是如果實在數字之後,就不需要補零。
標記設定完成後,如果需要補零,並且當前符號為符號,那就需要在前面補零,即往數值棧中新增一個零。
class Solution {
public int calculate(String s) {
Stack<Integer> numStack = new Stack();
Stack<Character> opsStack = new Stack();
s += " ";
String numStr= "";
boolean needZero = true;
for(char ch : s.toCharArray()){
if(ch >= '0' && ch <= '9'){
numStr += ch;
needZero = false;
continue;
}else if(!"".equals(numStr)){
numStack.push(Integer.parseInt(numStr));
numStr = "";
}
if(ch == ' '){
continue;
}
if(ch == '('){
opsStack.push(ch);
needZero = true;
continue;
}
if(ch == ')') {
while(opsStack.peek() != '('){
numStack.push(getResult(numStack.pop(), numStack.pop(), opsStack.pop()));
}
opsStack.pop();
continue;
}
if(ch == '-' && needZero){
numStack.push(0);
}
while(!opsStack.isEmpty() && getRange(ch) <= getRange(opsStack.peek())){
numStack.push(getResult(numStack.pop(), numStack.pop(), opsStack.pop()));
}
needZero = true;
opsStack.push(ch);
}
while(!opsStack.isEmpty()){
numStack.push(getResult(numStack.pop(), numStack.pop(), opsStack.pop()));
}
return numStack.pop();
}
private int getRange(char ch){
if(ch == '+' || ch == '-') {return 1;}
if(ch == '*' || ch == '/') {return 2;}
return 0;
}
private int getResult(int v1, int v2, char ops){
if(ops == '+') { return v2 + v1;}
if(ops == '-') { return v2 - v1;}
if(ops == '*') { return v2 * v1;}
return v2 / v1;
}
}
7、設計迴圈雙端佇列(中等)
題目地址:https://leetcode.cn/problems/design-circular-deque/
設計實現雙端佇列。
實現 MyCircularDeque
類:
-
MyCircularDeque(int k)
:建構函式,雙端佇列最大為k
。 -
boolean insertFront()
:將一個元素新增到雙端佇列頭部。 如果操作成功返回true
,否則返回false
。 -
boolean insertLast()
:將一個元素新增到雙端佇列尾部。如果操作成功返回true
,否則返回false
。 -
boolean deleteFront()
:從雙端佇列頭部刪除一個元素。 如果操作成功返回true
,否則返回false
。 -
boolean deleteLast()
:從雙端佇列尾部刪除一個元素。如果操作成功返回true
,否則返回false
。 -
int getFront()
):從雙端佇列頭部獲得一個元素。如果雙端佇列為空,返回-1
。 -
int getRear()
:獲得雙端佇列的最後一個元素。 如果雙端佇列為空,返回-1
。 -
boolean isEmpty()
:若雙端佇列為空,則返回true
,否則返回false
。 -
boolean isFull()
:若雙端佇列滿了,則返回true
,否則返回false
。
示例 1:
輸入
["MyCircularDeque", "insertLast", "insertLast", "insertFront", "insertFront", "getRear", "isFull", "deleteLast", "insertFront", "getFront"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
輸出
[null, true, true, true, false, 2, true, true, true, 4]
解釋
MyCircularDeque circularDeque = new MycircularDeque(3); // 設定容量大小為3
circularDeque.insertLast(1); // 返回 true
circularDeque.insertLast(2); // 返回 true
circularDeque.insertFront(3); // 返回 true
circularDeque.insertFront(4); // 已經滿了,返回 false
circularDeque.getRear(); // 返回 2
circularDeque.isFull(); // 返回 true
circularDeque.deleteLast(); // 返回 true
circularDeque.insertFront(4); // 返回 true
circularDeque.getFront(); // 返回 4
提示:
1 <= k <= 1000
0 <= value <= 1000
-
insertFront
,insertLast
,deleteFront
,deleteLast
,getFront
,getRear
,isEmpty
,isFull
呼叫次數不大於2000
次
解答:
沒什麼可說的
class MyCircularDeque {
ListNode headNode;
ListNode endNode;
int count;
int limit;
public MyCircularDeque(int k) {
headNode = new ListNode();
endNode = new ListNode();
headNode.next = endNode;
endNode.pre = headNode;
count = 0;
limit = k;
}
public boolean insertFront(int value) {
if(count == limit){
return false;
}
count += 1;
ListNode tempNode = headNode.next;
ListNode thisNode = new ListNode(value);
headNode.next = thisNode;
thisNode.pre = headNode;
thisNode.next = tempNode;
tempNode.pre = thisNode;
return true;
}
public boolean insertLast(int value) {
if(count == limit){
return false;
}
count += 1;
ListNode tempNode = endNode.pre;
ListNode thisNode = new ListNode(value);
tempNode.next = thisNode;
thisNode.pre = tempNode;
thisNode.next = endNode;
endNode.pre = thisNode;
return true;
}
public boolean deleteFront() {
if(count == 0){
return false;
}
count -= 1;
ListNode tempNode = headNode.next.next;
headNode.next = tempNode;
tempNode.pre = headNode;
return true;
}
public boolean deleteLast() {
if(count == 0){
return false;
}
count -= 1;
ListNode tempNode = endNode.pre.pre;
endNode.pre = tempNode;
tempNode.next = endNode;
return true;
}
public int getFront() {
return count == 0 ? -1 : headNode.next.val;
}
public int getRear() {
return count == 0 ? -1 : endNode.pre.val;
}
public boolean isEmpty() {
return count == 0;
}
public boolean isFull() {
return count == limit;
}
}
class ListNode{
public ListNode next;
public ListNode pre;
public int val;
public ListNode(int val){
this.val = val;
}
public ListNode(){
}
}
/**
* Your MyCircularDeque object will be instantiated and called as such:
* MyCircularDeque obj = new MyCircularDeque(k);
* boolean param_1 = obj.insertFront(value);
* boolean param_2 = obj.insertLast(value);
* boolean param_3 = obj.deleteFront();
* boolean param_4 = obj.deleteLast();
* int param_5 = obj.getFront();
* int param_6 = obj.getRear();
* boolean param_7 = obj.isEmpty();
* boolean param_8 = obj.isFull();
*/
四、單調棧與單調佇列
0、概述
單調佇列和單調棧並非一種資料結構,而是為了解題而出現的一種思路,將不規則的線性結構轉變為單調遞增或者單調遞減的規則線性結構,進而可以更好計算結果。
單調棧和單調佇列的解題套路:
(1)for 迴圈每一個選項
(2)while(棧頂與新元素不滿足單調性){彈棧,更新答案,累加"寬度"}
(3)入棧
1、柱狀圖中最大的矩形(困難)
題目地址:https://leetcode.cn/problems/largest-rectangle-in-histogram/
給定 n 個非負整數,用來表示柱狀圖中各個柱子的高度。每個柱子彼此相鄰,且寬度為 1 。
求在該柱狀圖中,能夠勾勒出來的矩形的最大面積。
示例 1:
輸入:heights = [2,1,5,6,2,3]
輸出:10
解釋:最大的矩形為圖中紅色區域,面積為 10
示例 2:
輸入: heights = [2,4]
輸出: 4
提示:
1 <= heights.length <=105
0 <= heights[i] <= 104
解答
解題思路:
1、為什麼選擇單調棧:如果陣列(柱子)是單調遞增,那麼就比較好處理:
如果從左開始,第 i 跟柱子時,height*(length-i),那麼迴圈每一根柱子,在每一跟柱子時計算面積,然後和既往最大值對比即可
其中 i 為當前節點的位置,length 為陣列長度,height 為 第 i 跟柱子的高度
如果從右開始,計算出當前節點的面積後,將高度設定為和前面的高度一致,再計算前面的高度
2、如何變為單調棧
這裡選擇單調遞增,那麼就是迴圈陣列,如果當前節點滿足單調性,就入棧,如果不滿足單調性,就進行處理(根據具體的題意進行處理,修正、拋棄、調整前一個等)讓其單調性成立,在這道題裡,就把之前滿足的做一個處理,保證之前節點的高度都小於當前節點的高度
由於是要計算最大面積,因此之前的拋棄時,要先計算之前的面積,例如現在是 2,3,4,1,那麼在 1 這個節點
之前的4已經不滿足單調性,那麼就應該把 4 拋棄,但是需要計算一下當前節點的最大面積,高度為4,寬度為1,面積為4
再繼續,3也不滿足單調性,拋棄3,在這時,高為3,寬為2(因為之前的4雖然拋棄,但是寬度仍然在),面積為6
再繼續,2也不滿足單調性,拋棄2,在這時,高為2,寬為3,面積為6
最終 2,3,4 都被拋棄,需要將 1 放入棧,放入的是高為 1,寬為 4 的節點(寬需要加上之前拋棄的所有節點的寬)
3、特殊處理
上面方法實際是保證了棧的單調性,但是最終的資料沒有處理,例如最終的單調棧為 2,3,4,5,那麼都是push到棧中了,並沒有計算最大值,或者沒有計算完畢,就需要從前或者從後再計算一遍。
為了避免上述再來一次迴圈導致程式碼臃腫,可以取個巧,就是在陣列的最後加一個0,讓其在最後的節點不滿足單調性,就會把之前的都計算了,所以,在 for 迴圈的判斷裡,使用了 i<= heights.leng 的條件(加上了等號),並且取第 i 個節點的高度時,如果是第 heights.length時,height 設定為 0,讓其前面的所有都不滿足單調性。
class Solution {
public int largestRectangleArea(int[] heights) {
Stack<Rect> stack = new Stack();
int ans = 0;
for(int i=0;i<= heights.length;i++){
int height = i== heights.length ? 0 : heights[i];
int sumWeight = 0;
while(!stack.isEmpty() && stack.peek().height >= height){
Rect rect = stack.pop();
sumWeight += rect.weight;
ans = Math.max(ans, sumWeight*rect.height);
}
stack.push(new Rect(sumWeight+1, height));
}
return ans;
}
}
class Rect{
public int weight;
public int height;
public Rect(int weight, int height){
this.weight = weight;
this.height = height;
}
}
2、滑動視窗最大值(困難)
題目地址:https://leetcode.cn/problems/sliding-window-maximum/
給你一個整數陣列 nums
,有一個大小為 k
的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k
個數字。滑動視窗每次只向右移動一位。
返回 滑動視窗中的最大值 。
示例 1:
輸入:nums = [1,3,-1,-3,5,3,6,7], k = 3
輸出:[3,3,5,5,6,7]
解釋:
滑動視窗的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
輸入:nums = [1], k = 1
輸出:[1]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
解答
題目分析:
以示例1為例,迴圈每一個節點:
i=0,值為1,可能最大值為1
i=1,值為3,由於1和3肯定在一組,因此1就沒有作用,最大值為3
i=2,值為-1,由於可能存在1,3,-1,-2,-3的情況,因此-1有可能成為後續的最大值
i=3,值為-3,與上面一樣,可能存在其為最大值的情況
i=4,值為5,那麼往前推兩位(k=3),前面兩位分別是-1和-3,那麼-1和-3就不可能成為最大值
綜上分析,如果使用一個單調佇列,保證其為單調遞減,那麼每一個節點都有可能成為後續的最大值,這個佇列是從後面新增資料的,但是還要保證取資料的區間為k的限制,如果頭結點的位置超過了當前節點與k的範圍,就要移除,也就是說,從而保證一定取到的是k範圍內的最大值。
解題:
由於要從兩端操作單調棧,那麼就可以使用一個雙端佇列,同時為了判斷最大值的位置,則雙端佇列中儲存的是最大值的下標,同時單調棧中保持單調遞減,那麼套用單調棧的套路,解題思路如下:
for 迴圈
如果單調棧中的頭結點超出了範圍,就剔除頭結點
while 判斷,對於不滿足單調性的節點處理,使其滿足單調性
獲取頭結點放入結果集
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] ans = new int[nums.length-k+1];
Deque<Integer> deque = new LinkedList();
for(int i=0;i<nums.length;i++){
if(!deque.isEmpty() && i-k >= deque.getFirst()){
deque.removeFirst();
}
while(!deque.isEmpty() && nums[deque.getLast()] <= nums[i]){
deque.removeLast();
}
deque.addLast(i);
if(i>=k-1){
ans[i-k+1] = nums[deque.getFirst()];
}
}
return ans;
}
}
3、接雨水(困難)
題目地址:https://leetcode.cn/problems/trapping-rain-water/
給定 n
個非負整數表示每個寬度為 1
的柱子的高度圖,計算按此排列的柱子,下雨之後能接多少雨水。
示例 1:
輸入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
輸出:6
解釋:上面是由陣列 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度圖,在這種情況下,可以接 6 個單位的雨水(藍色部分表示雨水)。
示例 2:
輸入:height = [4,2,0,3,2,5]
輸出:9
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
解答1:單調棧
分析思路:雨水可以向左或向右流動,直到碰到比他高的節點就會停止,如果兩端都碰到比他高的節點,那麼當前節點的水就能被收集,如果是這樣,保證單調遞減,那麼從第二個節點開始,都是有盛水的可能的,那麼如果碰到了不滿足單調遞減的節點,就可以去計算一次盛水量,因此本題可以採用單調遞減的方式處理。
解題套路仍然一樣,先迴圈所有節點,如果滿足單調性,則push,不滿足單調性,則針對該題的特殊場景做處理
不滿足單調性處理:不滿足單調性,說明前一個節點比當前節點低,那麼就要算出來前一個節點能存多少水,公式為:(Math.min(stack.peek().height, height) - rect.height) * sumWeight,其中stack.peek().height表示要前一個節點再之前一個節點的高度,height表示當前節點的高度,rect.height表示前一個節點的高度,sumWeight表示前一個節點的寬度。
可能描述起來比較繞口,舉例,5,3,6這個場景,到了6,是不滿足單調性的,因此就要計算當前節點(6)之前節點(3)可以盛多少水,那麼其可以盛3左右高低低的那一個,然後再減去3本身的高度,就可以計算其盛水高度,寬度即為3這個節點本身的寬度。
class Solution {
public int trap(int[] heights) {
Stack<Rect> stack = new Stack();
int ans = 0;
for(int height : heights){
int sumWeight = 0;
while(!stack.isEmpty() && stack.peek().height <= height){
Rect rect = stack.pop();
if(stack.isEmpty()){
continue;
}
sumWeight += rect.weight;
ans += (Math.min(stack.peek().height, height) - rect.height) * sumWeight;
}
stack.push(new Rect(sumWeight+1, height));
}
return ans;
}
}
class Rect{
public int weight;
public int height;
public Rect(int weight, int height){
this.weight = weight;
this.height = height;
}
}
解答2:字首最大值
一個更好理解的觀點:到一個節點後,檢視其前後的最大值,那麼最大值中的最小值如果大於當前節點的高度,則可以盛水,盛水的容量為左右最大值的小者與當前節點的高度差。
public int trap(int[] heights) {
int ans = 0;
int[] preMax = new int[heights.length];
int[] suffMax = new int[heights.length];
for(int i=1;i<heights.length;i++){
preMax[i] = Math.max(preMax[i-1], heights[i-1]);
}
for(int i = heights.length-2;i>=0;i--){
suffMax[i] = Math.max(suffMax[i+1], heights[i+1]);
System.out.println(suffMax[i]);
}
for(int i=1;i<heights.length-1;i++){
if(suffMax[i] > heights[i] && preMax[i] > heights[i]){
ans += Math.min(suffMax[i], preMax[i]) - heights[i];
}
}
return ans;
}
4、最大矩形(困難)
題目地址:https://leetcode.cn/problems/maximal-rectangle/
給定一個僅包含 0
和 1
、大小為 rows x cols
的二維二進位制矩陣,找出只包含 1
的最大矩形,並返回其面積。
示例 1:
輸入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
輸出:6
解釋:最大矩形如上圖所示。
示例 2:
輸入:matrix = []
輸出:0
示例 3:
輸入:matrix = [["0"]]
輸出:0
示例 4:
輸入:matrix = [["1"]]
輸出:1
示例 5:
輸入:matrix = [["0","0"]]
輸出:0
提示:
rows == matrix.length
cols == matrix[0].length
1 <= row, cols <= 200
-
matrix[i][j]
為'0'
或'1'
解答
這道題是第一題的一個擴充套件,迴圈每一行,將每一行變更為柱形陣列即可,那麼解題思路就變成了以下三步
1、迴圈每一行,將每一行變為一個柱形陣列:int[] heights = geneHeight(i, matrix);
2、計算每一行柱形陣列能得到的最大面積:max = getMax(heights)
3、比較每一行的最大面積,從主取出最大值:ans = Math.max(ans, max)
第2點,完成照搬第一題即可,第三點沒什麼可說的,就單獨說一下第一點
迴圈每一列,行都是從固定行開始,如果當前列的當前行不為1,當前柱形的高就加一,然後繼續向上遞迴,直到碰到0或者到了第一行,就計算出了當前列的高。
class Solution {
public int maximalRectangle(char[][] matrix) {
int ans = 0;
for(int i=0;i<matrix.length;i++){
int[] heights = geneHeight(i, matrix);
ans = Math.max(ans, getMax(heights));
}
return ans;
}
private int[] geneHeight(int row, char[][] matrix){
int col = matrix[row].length;
int[] heights = new int[col];
for(int i=0; i<col; i++){
int height = 0;
int rowTemp = row;
while(rowTemp >=0 && matrix[rowTemp][i] != '0'){
height++;
rowTemp--;
}
heights[i] = height;
}
return heights;
}
private int getMax(int[] heights){
Stack<Rect> stack = new Stack();
int ans = 0;
for(int i=0; i<=heights.length;i++){
int height = i == heights.length ? 0 : heights[i];
int sumWeight = 0;
while(!stack.isEmpty() && stack.peek().height >= height){
Rect rect = stack.pop();
sumWeight += rect.weight;
ans = Math.max(ans, sumWeight*rect.height);
}
stack.push(new Rect(sumWeight+1, height));
}
return ans;
}
}
class Rect{
public int weight;
public int height;
public Rect(int weight, int height){
this.weight = weight;
this.height = height;
}
}