O(1)時間複雜度來獲得佇列或棧的最大值或者最小值
該類題型通常是讓自己設計並封裝應用程式介面(Application Programming Interface, API),即一些預先定義的函式,或指軟體系統不同組成部分銜接的約定。
通常是使用常用的資料結構組合或是作為輔助空間來實現這些介面。具體選用何種資料結構來實現,一是依照題目中對時間或空間複雜度的要求,二是在平常的練習當中多加總結。
以下列出相關題目,題目中對時間複雜度的要求均為
O
(
1
)
O(1)
O(1),所以很容易想到用空間換取時間的思路。
(一) 劍指 Offer 30. 包含min函式的棧
思路:
普通棧的push()
pop()
函式的複雜度為
O
(
1
)
\mathcal{O}(1)
O(1);而獲取棧最小值min()
函式需要遍歷整個棧,複雜度為
O
(
n
)
\mathcal{O}(n)
O(n)。那麼將
min()
函式複雜度降為
O
(
1
)
\mathcal{O}(1)
O(1) ,可通過建立輔助棧實現;
- 資料棧 dataStk : 棧 dataStk 用於儲存所有元素,保證入棧 push() 函式、出棧 pop() 函式、獲取棧頂 top() 函式的正常邏輯;
- 輔助棧 minStk: 棧 minStk 中儲存棧 dataStk 中所有 非嚴格降序 的元素,則棧 dataStk 中的最小元素始終對應棧 minStk 的棧頂元素,即 min() 函式只需返回棧 minStk 的棧頂元素即可;
- 即只需設法維護好棧 minStk 的元素,使其保持非嚴格降序,即可實現 min() 函式的 O ( 1 ) \mathcal{O}(1) O(1)複雜度。
程式碼實現:
class MinStack {
public:
/** initialize your data structure here. */
MinStack() {}
void push(int x) {
//1.如果要push的數小於等於minStk的棧頂元素,則將x加入minStk
// 即保持minStk的非嚴格遞減特性
//2.如果此時minStk棧為空,則也將x加入minStk
if(!minStk.empty() && x <= minStk.top() || minStk.empty())
minStk.push(x);
dataStk.push(x);
}
void pop() {
//如果dataStk不為空,則彈出dataStk的棧頂元素
//如果彈出的dataStk的棧頂元素與minStk的棧頂元素相同,則也需要把minStk棧頂元素也彈出
if(!dataStk.empty()) {
if(dataStk.top() == minStk.top()) minStk.pop();
dataStk.pop();
}
}
int top() {
//棧的棧頂元素直接從dataStk彈出棧頂元素即可
return dataStk.top();
}
int min() {
//棧的最小元素直接從minStk彈出棧頂元素即可
return minStk.top();
}
private:
stack<int> dataStk;
stack<int> minStk;
};
(二) 劍指 Offer 59 - II. 佇列的最大值
思路:
對於一個普通佇列,push_back
和pop_front
的時間複雜度都是
O
(
1
)
\mathcal{O}(1)
O(1),因此直接使用佇列的相關操作就可以實現這兩個函式。
對於這種最值問題,可以聯想站隊問題。對於一個佇列,我們從隊尾往隊首看,若是第
i
i
i個人的是
[
1
,
i
]
[1, i]
[1,i]位置的人身高最高的,則這個佇列的最大身高就為第
i
i
i位置的人身高,佇列前面的人(編號為
[
1
,
i
−
1
]
[1, i-1]
[1,i−1])無論走幾個,這個佇列的最大身高一直是第
i
i
i個人的身高。
類比一下,維護一個非嚴格遞減的佇列,隊首元素即為佇列的最大值,且是
O
(
1
)
\mathcal{O}(1)
O(1)時間找到的。(可以結合單調棧進行理解,這裡是單調佇列)
可以看到,輔助佇列deque中的隊首元素始終是當前佇列的最大值。
程式碼實現:
class MaxQueue {
public:
MaxQueue() {}
//求佇列的最大值,若maxDeq非空,直接返回隊首元素,否則返回-1
int max_value() {
if(!maxDeq.empty()) return maxDeq.front();
else return -1;
}
//向佇列中新增元素
void push_back(int value) {
dataDeq.push_back(value); //資料佇列中直接新增
if(maxDeq.empty()) { //若是輔助佇列為空,直接新增到輔助佇列中
maxDeq.push_back(value);
return;
}
//如果說maxDeq非空且佇列的隊尾元素小於當前要加入的元素
//隊尾元素一直出隊,直到隊尾元素的值不小於要加入的元素的值
while(!maxDeq.empty() && maxDeq.back() < value)
maxDeq.pop_back();
maxDeq.push_back(value); //將要加入的元素加入輔助佇列
}
//彈出隊首元素
int pop_front() {
if(!dataDeq.empty()) { //如果資料佇列非空,同時能保證輔助佇列也非空
int ret = dataDeq.front(); //儲存當前的資料佇列隊首元素
dataDeq.pop_front();
if(maxDeq.front() == ret) //如果資料佇列隊首元素與輔助佇列隊首元素相同
maxDeq.pop_front(); //將輔助佇列的隊首元素也要彈出
return ret;
}
return -1; //如果資料佇列為空,則無操作,並返回-1
}
private:
deque<int> dataDeq; //資料佇列
deque<int> maxDeq; //輔助佇列
};
類似的題,有劍指 Offer 59 - I. 滑動視窗的最大值,這裡維護一個滑動視窗長度的最值佇列,假設當前佇列中元素的下標範圍為 [ i − k , i ] [i-k, i] [i−k,i],若是nums[i-1]和當前佇列的隊首元素相同,則最值佇列彈出隊首元素;然後將隊尾元素與 n u m s [ i ] nums[i] nums[i]進行比較,若是小於則彈出隊尾元素直至新的隊尾元素不小於 n u m s [ i ] nums[i] nums[i]為止;最終再加入 n u m s [ i ] nums[i] nums[i],並將當前隊首元素記錄在當前視窗的最大值記錄容器中即可。
(三) 146. LRU 快取機制
思路:
當時做這道題想成了使用佇列的方法,結果無法解決多次get的問題,整了大半天,只能通過一半的用例,真是欲哭無淚,選用合適的資料結構是相當重要啊!但是實現過程中,已經想到了最近使用過的頁面放到佇列/資料結構一端的思想,與解法是相同的。
該題應當組合使用雙向連結串列和雜湊表:
- 雙向連結串列按照頁面的使用順序進行排列,表首位置表示最近剛使用過的頁面,表尾位置表示最近最少使用的頁面,若是連結串列長度超過快取容量,則先刪除表尾節點再在表首新增新的節點。注意到,對連結串列的刪除,頭部新增節點和尾部刪除節點都是 O ( 1 ) \mathcal{O}(1) O(1)的時間複雜度;
- 雜湊表用於儲存鍵值和連結串列節點的對應關係,以便在 O ( 1 ) \mathcal{O}(1) O(1)時間內找到連結串列並進行操作;
- 以上思路,保證了對於 put 和 get 都是 O ( 1 ) \mathcal{O}(1) O(1)時間複雜度,空間複雜度為 O ( capacity ) \mathcal{O}(\text{capacity}) O(capacity),因為雜湊表和雙向連結串列最多儲存 capacity + 1 \text{capacity} + 1 capacity+1個元素。
注意:在雙向連結串列的實現中,使用一個偽頭部(dummy head)和偽尾部(dummy tail)標記界限,這樣在新增節點和刪除節點的時候就不需要檢查相鄰的節點是否存在。在單鏈表中,常使用偽頭部節點,排除空連結串列的情景,簡化程式設計情況。
程式碼實現:
//自定義雙向連結串列,包含其前節點和後節點,鍵值對和結構體建構函式
struct DLinkedNode {
int key, value;
DLinkedNode* prev;
DLinkedNode* next;
//使用預設引數列表對結構體成員進行預設初始化
DLinkedNode():key(0), value(0), prev(nullptr), next(nullptr) {}
//帶引數的初始化方式
DLinkedNode(int key, int value):key(key), value(value), prev(nullptr), next(nullptr) {}
};
class LRUCache {
public:
//LRUCache建構函式,構造雙向連結串列中的偽頭節點和偽尾節點,並初始化容量值,當前使用容量值
LRUCache(int capacity) : capacity(capacity), size(0) {
this->head = new DLinkedNode();
this->tail = new DLinkedNode();
this->head->next = tail;
this->tail->prev = head;
}
//get函式,獲取由頁面的key值獲取頁面的value值
int get(int key) {
//如果在快取中能夠找到key值對應的頁面
if(cache.find(key) != cache.end()) {
moveToHead(cache[key]); //將該頁面移至隊首
return cache[key]->value; //返回key對應的value
} else return -1; //若是沒有key值對應的頁面,則返回-1
}
void put(int key, int value) {
if(cache.find(key) != cache.end()) {
//如果key存在,更新key對應的value值,再將節點移至表首
cache[key]->value = value;
moveToHead(cache[key]);
} else {
//如果key不存在,建立新的連結串列節點
DLinkedNode* newNode = new DLinkedNode(key, value);
addToHead(newNode); //將新的連結串列節點加到表首
cache[key] = newNode; //更新快取雜湊表
++size; //已用容量+1
if(size > capacity) { //若是已用容量超出容量,將表尾節點刪去
DLinkedNode* delNode = removeTail();
cache.erase(delNode->key); //在雜湊表中刪除該節點
size--; //更新已用容量
delete delNode; //刪除該節點,防止記憶體洩漏
}
}
}
private:
unordered_map<int, DLinkedNode*> cache;
DLinkedNode* head;
DLinkedNode* tail;
int size;
int capacity;
void addToHead(DLinkedNode* node) {
node->next = head->next;
node->prev = head;
head->next = node;
node->next->prev = node;
}
void removeNode(DLinkedNode* node) {
node->prev->next = node->next;
node->next->prev = node->prev;
node->prev = nullptr;
node->next = nullptr;
}
void moveToHead(DLinkedNode* node) {
removeNode(node);
addToHead(node);
}
DLinkedNode* removeTail() {
DLinkedNode* node = tail->prev;
removeNode(node);
return node;
}
};