什么是棧(Stack)?
棧(stack)是一種采用后進先出(LIFO,last in first out)策略的抽象數據結構。比如物流裝車,后裝的貨物先卸,先轉的貨物后卸。棧在數據結構中的地位很重要,在算法中的應用也很多,比如用於非遞歸的遍歷二叉樹,計算逆波蘭表達式,等等。
棧一般用一個存儲結構(常用數組,偶見鏈表),存儲元素。並用一個指針記錄棧頂位置。棧底位置則是指棧中元素數量為0時的棧頂位置,也即棧開始的位置。
棧的主要操作:
push()
,將新的元素壓入棧頂,同時棧頂上升。pop()
,將新的元素彈出棧頂,同時棧頂下降。empty()
,棧是否為空。peek()
,返回棧頂元素。
在各語言的標准庫中:
- Java,使用
java.util.Stack
,它是擴展自Vector
類,支持push()
,pop()
,peek()
,empty()
,search()
等操作。 - C++,使用
<stack>
中的stack
即可,方法類似Java,只不過C++中peek()
叫做top()
,而且pop()
時,返回值為空。 - Python,直接使用
list
,查看棧頂用[-1]
這樣的切片操作,彈出棧頂時用list.pop()
,壓棧時用list.append()
。
如何自己實現一個棧?
參見問題:http://www.lintcode.com/en/problem/implement-stack/
這里給出一種用ArrayList
的通用實現方法。(注意:為了將重點放在棧結構上,做了適當簡化。該棧僅支持整數類型,若想實現泛型,可用反射機制和object對象傳參;此外,可多做安全檢查並拋出異常)
Python
class Stack: def __init__(self): self.array = [] # 壓入新元素 def push(self, x): self.array.append(x) # 棧頂元素出棧 def pop(self): if not self.isEmpty(): self.array.pop() # 返回棧頂元素 def top(self): return self.array[-1] # 判斷是否是空棧 def isEmpty(self): return len(self.array) == 0
棧在計算機內存當中的應用
我們在程序運行時,常說的內存中的堆棧,其實就是棧空間。這一段空間存放着程序運行時,產生的各種臨時變量、函數調用,一旦這些內容失去其作用域,就會被自動銷毀。
函數調用其實是棧的很好的例子,后調用的函數先結束,所以為了調用函數,所需要的內存結構,棧是再合適不過了。在內存當中,棧從高地址不斷向低地址擴展,隨着程序運行的層層深入,棧頂指針不斷指向內存中更低的地址。
相關參考資料:
https://blog.csdn.net/liu_yude/article/details/45058687
我們在前面的二叉樹的學習中,已經學習了如何使用 Stack 來進行非遞歸的二叉樹遍歷。
這里我們來看看棧在面試中的其他一些考點和考題:
- 如果自己實現一個棧?
- 如何用兩個隊列實現一個棧?
- 用一個數組如何實現三個棧?
隊列
定義
- 隊列為一種先進先出的線性表
- 只允許在表的一端進行入隊,在另一端進行出隊操作。在隊列中,允許插入的一端叫隊尾,允許刪除的一端叫隊頭,即入隊只能從隊尾入,出隊只能從隊頭出
思路
- 需要兩個節點,一個頭部節點,也就是dummy節點,它是在加入的第一個元素的前面,也就是dummy.next=第一個元素,這樣做是為了方便我們刪除元素,還有一個尾部節點,也就是tail節點,表示的是最后一個元素的節點
- 初始時,tail節點跟dummy節點重合
- 當我們要加入一個元素時,也就是從隊尾中加入一個元素,只需要新建一個值為val的node節點,然后tail.next=node,再移動tail節點到tail.next
- 當我們需要刪除隊頭元素時,只需要將dummy.next變為dummy.next.next,這樣就刪掉了第一個元素,這里需要注意的是,如果刪掉的是隊列中唯一的一個元素,那么需要將tail重新與dummy節點重合
- 當我們需要得到隊頭元素而不刪除這個元素時,只需要獲得dummy.next.val就可以了
示例代碼
Python:
class QueueNode: def __init__(self, value): self.val = value self.next = None class Queue: def __init__(self): self.dummy = QueueNode(-1) self.tail = self.dummy def enqueue(self, val): node = QueueNode(val) self.tail.next = node self.tail = node def dequeue(self): ele = self.dummy.next.val self.dummy.next = self.dummy.next.next if not self.dummy.next: self.tail = self.dummy return ele def peek(self): return self.dummy.next.val def isEmpty(self): return self.dummy.next == None
隊列通常被用作 BFS 算法的主要數據結構。除此之外面試中其他可能考察隊列這個數據結構的地方並不多。我們這里把非 BFS 的隊列考題考點做一個匯總:
- 如何用鏈表實現隊列?
- 如何用兩個棧實現一個隊列?
- 什么是循環數組,如何用循環數組實現隊列?
什么是循環數組,如何用循環數組實現隊列?
什么是循環數組
Circular array = a data structure that used a array as if it were connected end-to-end
可以圖示為:
如何實現隊列
- 我們需要知道隊列的入隊操作是只在隊尾進行的,相對的出隊操作是只在隊頭進行的,所以需要兩個變量front與rear分別來指向隊頭與隊尾
- 由於是循環隊列,我們在增加元素時,如果此時 rear = array.length - 1 ,rear 需要更新為 0;同理,在元素出隊時,如果 front = array.length - 1, front 需要更新為 0. 對此,我們可以通過對數組容量取模來更新。
示例代碼
Python:
class CircularQueue: def __init__(self, n): self.circularArray = [0]*n self.front = 0 self.rear = 0 self.size = 0 """ @return: return true if the array is full """ def isFull(self): return self.size == len(self.circularArray) """ @return: return true if there is no element in the array """ def isEmpty(self): return self.size == 0 """ @param element: the element given to be added @return: nothing """ def enqueue(self, element): if self.isFull(): raise RuntimeError("Queue is already full") self.rear = (self.front+self.size) % len(self.circularArray) self.circularArray[self.rear] = element self.size += 1 """ @return: pop an element from the queue """ def dequeue(self): if self.isEmpty(): raise RuntimeError("Queue is already empty") ele = self.circularArray[self.front] self.front = (self.front+1) % len(self.circularArray) self.size -= 1 return ele
在線練習
http://www.lintcode.com/zh-cn/problem/implement-queue-by-circular-array/#
642. 數據流滑動窗口平均值
給出一串整數流和窗口大小,計算滑動窗口中所有整數的平均值。
樣例
樣例1 :
MovingAverage m = new MovingAverage(3); m.next(1) = 1 // 返回 1.00000 m.next(10) = (1 + 10) / 2 // 返回 5.50000 m.next(3) = (1 + 10 + 3) / 3 // 返回 4.66667 m.next(5) = (10 + 3 + 5) / 3 // 返回 6.00000
from collections import deque class MovingAverage: """ @param: size: An integer """ def __init__(self, size): # do intialization if necessary self.q = deque() self.size = size self.sum = 0 """ @param: val: An integer @return: """ def next(self, val): # write your code here if len(self.q) == self.size: self.sum -= self.q.popleft() self.q.append(val) self.sum += val return self.sum/len(self.q) # Your MovingAverage object will be instantiated and called as such: # obj = MovingAverage(size) # param = obj.next(val)
134. LRU緩存策略
為最近最少使用(LRU)緩存策略設計一個數據結構,它應該支持以下操作:獲取數據和寫入數據。
get(key)
獲取數據:如果緩存中存在key,則獲取其數據值(通常是正數),否則返回-1。set(key, value)
寫入數據:如果key還沒有在緩存中,則寫入其數據值。當緩存達到上限,它應該在寫入新數據之前刪除最近最少使用的數據用來騰出空閑位置。
最終, 你需要返回每次 get
的數據.
樣例
樣例 1:
輸入:
LRUCache(2)
set(2, 1)
set(1, 1)
get(2)
set(4, 1)
get(1)
get(2)
輸出:[1,-1,1]
解釋:
cache上限為2,set(2,1),set(1, 1),get(2) 然后返回 1,set(4,1) 然后 delete (1,1),因為 (1,1)最少使用,get(1) 然后返回 -1,get(2) 然后返回 1。
樣例 2:
輸入:
LRUCache(1)
set(2, 1)
get(2)
set(3, 2)
get(2)
get(3)
輸出:[1,-1,2]
解釋:
cache上限為 1,set(2,1),get(2) 然后返回 1,set(3,2) 然后 delete (2,1),get(2) 然后返回 -1,get(3) 然后返回 2。
from collections import deque class LRUCache: """ @param: capacity: An integer """ def __init__(self, capacity): # do intialization if necessary self.cap = capacity self.q = deque() self.cache = dict() def move2tail(self, key): self.q.remove(key) self.q.append(key) """ @param: key: An integer @return: An integer """ def get(self, key): # write your code here if key in self.cache: self.move2tail(key) return self.cache[key] else: return -1
""" @param: key: An integer @param: value: An integer @return: nothing """ def set(self, key, value): # write your code here if key in self.cache: self.cache[key] = value self.move2tail(key) else: if len(self.q) == self.cap: del self.cache[self.q.popleft()] self.q.append(key) self.cache[key] = value
這個實現的時間復雜度有點高,更新的時候是O(n),使用hash+雙向鏈表是最優的思路,其中hash存儲的是鏈表的節點地址。 還是有點繁瑣,代碼較多,一次性寫對比較難,我自己寫的:
class Node(object): def __init__(self, val=None, pre=None, next=None): self.val = val self.pre = pre self.next = next class LinkList(object): def __init__(self): self.dummy = Node() self.tail = None self.size = 0 def append(self, value): new_node = Node(value) if self.tail is None: self.tail = new_node self.dummy.next = new_node new_node.pre = self.dummy else: self.tail.next = new_node new_node.pre = self.tail self.tail = new_node self.size += 1 return new_node def popleft(self): pop_node = self.dummy.next self.remove(pop_node) return pop_node def remove(self, node): if not node: return pre_node = node.pre next_node = node.next if pre_node: pre_node.next = next_node if next_node: next_node.pre = pre_node else: self.tail = pre_node self.size -= 1 def __len__(self): return self.size class LRUCache: """ @param: capacity: An integer """ def __init__(self, capacity): # do intialization if necessary self.cap = capacity self.q = LinkList() self.cache = dict() def move2tail(self, key, value=None): node = self.cache[key] assert node.val[0] == key self.q.remove(node) if value is None: value = node.val[1] new_node = self.q.append((key, value)) self.cache[key] = new_node return new_node """ @param: key: An integer @return: An integer """ def get(self, key): # write your code here if key in self.cache: node = self.move2tail(key) return node.val[1] else: return -1 """ @param: key: An integer @param: value: An integer @return: nothing """ def set(self, key, value): # write your code here if key in self.cache: self.move2tail(key, value) else: if len(self.q) == self.cap: node = self.q.popleft() del self.cache[node.val[0]] node = self.q.append((key, value)) self.cache[key] = node
還有單鏈表的做法:
如下(單鏈表):
class LinkedNode: def __init__(self, key=None, value=None, next=None): self.key = key self.value = value self.next = next class LRUCache: # @param capacity, an integer def __init__(self, capacity): self.key_to_prev = {} self.dummy = LinkedNode() self.tail = self.dummy self.capacity = capacity def push_back(self, node): self.key_to_prev[node.key] = self.tail self.tail.next = node self.tail = node def pop_front(self): # 刪除頭部 head = self.dummy.next del self.key_to_prev[head.key] self.dummy.next = head.next self.key_to_prev[head.next.key] = self.dummy # change "prev->node->next...->tail" # to "prev->next->...->tail->node" def kick(self, prev): #將數據移動至尾部 node = prev.next if node == self.tail: return # remove the current node from linked list prev.next = node.next # update the previous node in hash map self.key_to_prev[node.next.key] = prev node.next = None self.push_back(node) # @return an integer def get(self, key): #獲取數據 if key not in self.key_to_prev: return -1 self.kick(self.key_to_prev[key]) return self.key_to_prev[key].next.value # @param key, an integer # @param value, an integer # @return nothing def set(self, key, value): #數據放入緩存 if key in self.key_to_prev: self.kick(self.key_to_prev[key]) self.key_to_prev[key].next.value = value return self.push_back(LinkedNode(key, value)) #如果key不存在,則存入新節點 if len(self.key_to_prev) > self.capacity: #如果緩存超出上限 self.pop_front() #刪除頭部
960. 數據流中第一個獨特的數 II
我們需要實現一個叫 DataStream
的數據結構。並且這里有 兩
個方法需要實現:
void add(number)
// 加一個新的數int firstUnique()
// 返回第一個獨特的數
樣例
例1:
輸入:
add(1)
add(2)
firstUnique()
add(1)
firstUnique()
輸出:
[1,2]
例2:
輸入:
add(1)
add(2)
add(3)
add(4)
add(5)
firstUnique()
add(1)
firstUnique()
add(2)
firstUnique()
add(3)
firstUnique()
add(4)
firstUnique()
add(5)
add(6)
firstUnique()
輸出:
[1,2,3,4,5,6]
注意事項
你可以假設在調用 firstUnique 方法時,數據流中至少有一個獨特的數字
""" 面試官期望的解法:使用 Hash & Linked List 處理 Data Stream 的問題 使用類似 LRU Cache 的做法來做。 hash 的 key 為 num,value 為對應的 linkedlist 上的 previous node linkedlist 記錄的是不重復的元素 """ class DataStream: def __init__(self): self.dummy = ListNode(0) self.tail = self.dummy self.num_to_prev = {} self.duplicates = set() """ @param num: next number in stream @return: nothing """ def add(self, num): if num in self.duplicates: return if num not in self.num_to_prev: self.push_back(num) return # find duplicate, remove it from hash & linked list self.duplicates.add(num) self.remove(num) def remove(self, num): prev = self.num_to_prev.get(num) del self.num_to_prev[num] prev.next = prev.next.next if prev.next: self.num_to_prev[prev.next.val] = prev else: # if we removed the tail node, prev will be the new tail self.tail = prev def push_back(self, num): # new num add to the tail self.tail.next = ListNode(num) self.num_to_prev[num] = self.tail self.tail = self.tail.next """ @return: the first unique number in stream """ def firstUnique(self): if not self.dummy.next: return None return self.dummy.next.val
657. Insert Delete GetRandom O(1)
設計一個數據結構實現在平均 O(1)
的復雜度下執行以下所有的操作。
-
insert(val)
: 如果這個元素不在set中,則插入。 -
remove(val)
: 如果這個元素在set中,則從set中移除。 -
getRandom
: 隨機從set中返回一個元素。每一個元素返回的可能性必須相同。
樣例
// 初始化空集set
RandomizedSet randomSet = new RandomizedSet();
// 1插入set中。返回正確因為1被成功插入
randomSet.insert(1);
// 返回錯誤因為2不在set中
randomSet.remove(2);
// 2插入set中,返回正確,set現在有[1,2]。
randomSet.insert(2);
// getRandom 應該隨機的返回1或2。
randomSet.getRandom();
// 從set中移除1,返回正確。set現在有[2]。
randomSet.remove(1);
// 2已經在set中,返回錯誤。
randomSet.insert(2);
// 因為2是set中唯一的數字,所以getRandom總是返回2。
randomSet.getRandom();
使用數組來保存當前集合中的元素,同時用一個hashMap來保存數字與它在數組中下標的對應關系。
插入操作時:
- 若已存在此元素返回false
- 不存在時將新的元素插入數組最后一位,同時更新hashMap。
刪除操作時:
- 若不存在此元素返回false
- 存在時先根據hashMap得到要刪除數字的下標,再將數組的最后一個數放到需要刪除的數的位置上,刪除數組最后一位,同時更新hashMap。
獲取隨機數操作時:
- 根據數組的長度來獲取一個隨機的下標,再根據下標獲取元素。
from random import randint class RandomizedSet: def __init__(self): # do intialization if necessary self.pos_map = {} self.elements = [] self.size = 0 """ @param: val: a value to the set @return: true if the set did not already contain the specified element or false """ def insert(self, val): # write your code here if val in self.pos_map: return True if len(self.elements) == self.size: self.elements.append(val) else: self.elements[self.size] = val self.pos_map[val] = self.size self.size += 1 return False """ @param: val: a value from the set @return: true if the set contained the specified element or false """ def remove(self, val): # write your code here if val not in self.pos_map: return False pos = self.pos_map[val] del self.pos_map[val] last_element = self.elements[self.size-1] self.elements[pos] = last_element self.pos_map[last_element] = pos self.size -= 1 return True """ @return: Get a random element from the set """ def getRandom(self): # write your code here return self.elements[randint(0, self.size-1)] # Your RandomizedSet object will be instantiated and called as such: # obj = RandomizedSet() # param = obj.insert(val) # param = obj.remove(val) # param = obj.getRandom()
當然,使用self.nums.pop()也可以,就不用再記錄size了。
如果是非重復元素,
954. Insert Delete GetRandom O(1) - 允許重復
設計一個數據結構,需要支持以下操作,且平均時間復雜度為 O(1)。
樣例
樣例 1:
輸入:
insert(1)
insert(1)
insert(2)
getRandom()
remove(1)
// 初始化一個空的容器
RandomizedCollection collection = new RandomizedCollection();
// 把 1 插入到這容器中。如果這個容器不含 1 則返回 true
collection.insert(1);
// 把 1 插入到這容器中。如果這個容器含 1 則返回 false, 此時容器中含有 [1,1]。
collection.insert(1);
// 把 1 插入到這容器中, 返回 true, 此時容器中含有 [1,1,2].
collection.insert(2);
// getRandom 應該有 2/3 的概率返回 1 ,1/3 的概率返回 2。
collection.getRandom();
// 從容器中刪去一個 1 , 此時容器中含有 [1,2].
collection.remove(1);
// getRandom 應該可以同等概率的返回 1 和 2。
collection.getRandom();
樣例 2:
輸入:
insert(1)
insert(1)
getRandom()
remove(1)
注意事項
允許有重復元素
則:
基本算法是不變的,只不過需要一個辦法來處理重復的數。
這里的解決辦法是,用 HashMap 存儲 number to a list of indices in numbers array. 也就是說,把某個數在數組中出現的所有的位置用 List 的形式存儲下來
這樣子的話,刪除一個數的時候,就是從這個 list 里隨便拿走一個數(比如最后一個數)
但是還需要解決的問題是,原來的算法中,刪除一個數的時候,需要拿 number array 的最后個位置的數,來覆蓋掉被刪除的數。那這樣原本在最后一個位置的數,他在 HashMap 里的記錄就應該相應的變化。
但是,我們只能得到這個移動了的數是什么,而這個被移動過的數,可能出現在好多個位置上,去要從 HashMap 里得到的 indices 列表里,改掉那個 index=當前最后一個位置的下標。
所以這里的做法是,修改 number array,不只是存儲 Number,同時存儲,這個數在 HashMap 的 indices 列表中是第幾個數。這樣就能直接去改那個數的值了。否則還得 for 循環一次,就無法做到 O(1)
import random import collections class RandomizedCollection(object): def __init__(self): """ Initialize your data structure here. """ self.elements = [] self.indexes = collections.defaultdict(set) def insert(self, val): """ Inserts a value to the collection. Returns true if the collection did not already contain the specified element. :type val: int :rtype: bool """ self.elements.append(val) ans = False if val in self.indexes: ans = True self.indexes[val].add(len(self.elements)-1) return ans """ just choose one element in set data """ def choose_one(self, set_data): for i in set_data: return i def remove(self, val): """ Removes a value from the collection. Returns true if the collection contained the specified element. :type val: int :rtype: bool """ if val in self.indexes and self.indexes[val]: last_pos = len(self.elements)-1 last_element = self.elements[last_pos] assert last_pos in self.indexes[last_element] self.indexes[last_element].remove(last_pos) if val != last_element: pos = self.choose_one(self.indexes[val]) self.indexes[last_element].add(pos) self.indexes[val].remove(pos) self.elements[pos] = last_element self.elements.pop() return True else: return False def getRandom(self): """ Get a random element from the collection. :rtype: int """ return self.elements[random.randint(0, len(self.elements)-1)] # Your RandomizedCollection object will be instantiated and called as such: # obj = RandomizedCollection() # param_1 = obj.insert(val) # param_2 = obj.remove(val) # param_3 = obj.getRandom()
哈希表 Hash
哈希表(Java 中的 HashSet / HashMap,C++ 中的 unordered_map,Python 中的 dict)是面試中非常常見的數據結構。它的主要考點有兩個:
- 是否會靈活的使用哈希表解決問題
- 是否熟練掌握哈希表的基本原理
這一小節中,我們將介紹一下哈希表原理中的幾個重要的知識點:
- 哈希表的工作原理
- 為什么 hash 上各種操作的時間復雜度不能單純的認為是 O(1) 的
- 哈希函數(Hash Function)該如何實現
- 哈希沖突(Collision)該如何解決
- 如何讓哈希表可以不斷擴容?
在數據結構中,哈希函數是用來將一個字符串(或任何其他類型)轉化為小於哈希表大小且大於等於零的整數。一個好的哈希函數可以盡可能少地產生沖突。一種廣泛使用的哈希函數算法是使用數值33,假設任何字符串都是基於33的一個大整數,比如:
hashcode("abcd") = (ascii(a) * 333 + ascii(b) * 332 + ascii(c) *33 + ascii(d)) % HASH_SIZE
= (97* 333 + 98 * 332 + 99 * 33 +100) % HASH_SIZE
= 3595978 % HASH_SIZE
其中HASH_SIZE表示哈希表的大小(可以假設一個哈希表就是一個索引0 ~ HASH_SIZE-1的數組)。
給出一個字符串作為key和一個哈希表的大小,返回這個字符串的哈希值。
class Solution: """ @param key: A String you should hash @param HASH_SIZE: An integer @return an integer """ def hashCode(self, key, HASH_SIZE): # write your code here ans = 0 for x in key: ans = (ans * 33 + ord(x)) % HASH_SIZE return ans
取模過程要使用同余定理:
(a * b ) % MOD = ((a % MOD) * (b % MOD)) % MOD
沖突(Collision),是說兩個不同的 key 經過哈希函數的計算后,得到了兩個相同的值。解決沖突的方法,主要有兩種:
- 開散列法(Open Hashing)。是指哈希表所基於的數組中,每個位置是一個 Linked List 的頭結點。這樣沖突的 <key, value> 二元組,就都放在同一個鏈表中。
- 閉散列法(Closed Hashing)。是指在發生沖突的時候,后來的元素,往下一個位置去找空位。
哈希表容量的大小在一開始是不確定的。如果哈希表存儲的元素太多(如超過容量的十分之一),我們應該將哈希表容量擴大一倍,並將所有的哈希值重新安排。假設你有如下一哈希表:
size=3
, capacity=4
[null, 21, 14, null]
↓ ↓
9 null
↓
null
哈希函數為:
int hashcode(int key, int capacity) {
return key % capacity;
}
這里有三個數字9,14,21,其中21和9共享同一個位置因為它們有相同的哈希值1(21 % 4 = 9 % 4 = 1)。我們將它們存儲在同一個鏈表中。
重建哈希表,將容量擴大一倍,我們將會得到:
size=3
, capacity=8
index: 0 1 2 3 4 5 6 7
hash : [null, 9, null, null, null, 21, 14, null]
給定一個哈希表,返回重哈希后的哈希表。
class Solution: def addlistnode(self, node, number): if node.next != None: self.addlistnode(node.next, number) else: node.next = ListNode(number) def addnode(self, anshashTable, number): p = number % len(anshashTable) if anshashTable[p] == None: anshashTable[p] = ListNode(number) else: self.addlistnode(anshashTable[p], number) def rehashing(self,hashTable): HASH_SIZE = 2 * len(hashTable) anshashTable = [None for i in range(HASH_SIZE)] for item in hashTable: p = item while p != None: self.addnode(anshashTable,p.val) p = p.next return anshashTable
138. 子數組之和
給定一個整數數組,找到和為零的子數組。你的代碼應該返回滿足要求的子數組的起始位置和結束位置
樣例
樣例 1:
輸入: [-3, 1, 2, -3, 4]
輸出: [0,2] 或 [1,3]
樣例解釋: 返回任意一段和為0的區間即可。
樣例 2:
輸入: [-3, 1, -4, 2, -3, 4]
輸出: [1,5]
注意事項
至少有一個子數組的和為 0
用前綴和 某一段[l, r]的和為0, 則其對應presum[l-1] = presum[r].
presum 為數組前綴和。只要保存每個前綴和,找是否有相同的前綴和即可。
class Solution: """ @param nums: A list of integers @return: A list of integers includes the index of the first number and the index of the last number """ def subarraySum(self, nums): prefix_hash = {0: -1} prefix_sum = 0 for i, num in enumerate(nums): prefix_sum += num if prefix_sum in prefix_hash: return prefix_hash[prefix_sum]+1, i prefix_hash[prefix_sum] = i return -1, -1
i-1, 出錯???沒有搞懂。
105. 復制帶隨機指針的鏈表
給出一個鏈表,每個節點包含一個額外增加的隨機指針可以指向鏈表中的任何節點或空的節點。
返回一個深拷貝的鏈表。
挑戰
可否使用O(1)的空間
""" Definition for singly-linked list with a random pointer. class RandomListNode: def __init__(self, x): self.label = x self.next = None self.random = None """ class Solution: # @param head: A RandomListNode # @return: A RandomListNode def copyRandomList(self, head): # write your code here if not head: return None copied_head, map_nodes = self.copy_nodes(head) self.copy_link(head, map_nodes) return copied_head def copy_nodes(self, head): dummy = RandomListNode(None) # Tips node, pre = head, dummy map_nodes = {} while node: node_cp = RandomListNode(node.label) map_nodes[node] = node_cp pre.next = node_cp node = node.next pre = node_cp return dummy.next, map_nodes def copy_link(self, head, map_nodes): node = head while node: if node.random: map_nodes[node].random = map_nodes[node.random] node = node.next
124. 最長連續序列
給定一個未排序的整數數組,找出最長連續序列的長度。
樣例
樣例 1
輸入 : [100, 4, 200, 1, 3, 2]
輸出 : 4
解釋 : 這個最長的連續序列是 [1, 2, 3, 4]. 返回所求長度 4
說明
要求你的算法復雜度為O(n)
class Solution: """ @param num: A list of integers @return: An integer """ def longestConsecutive(self, num): # write your code here ans = 1 uniq_num_visted = {n: False for n in num} for n in uniq_num_visted: if uniq_num_visted[n]: continue uniq_num_visted[n] = True while n + 1 in uniq_num_visted: uniq_num_visted[n + 1] = True n += 1 high = n while n - 1 in uniq_num_visted: uniq_num_visted[n - 1] = True n -= 1 low = n ans = max(ans, high-low+1) return ans
Heap 的結構和原理
基於 Siftup 的版本 O(nlogn)
Python版本:
import sys import collections class Solution: # @param A: Given an integer array # @return: void def siftup(self, A, k): while k != 0: father = (k - 1) // 2 if A[k] > A[father]: break temp = A[k] A[k] = A[father] A[father] = temp k = father def heapify(self, A): for i in range(len(A)): self.siftup(A, i)
算法思路:
- 對於每個元素A[i],比較A[i]和它的父親結點的大小,如果小於父親結點,則與父親結點交換。
- 交換后再和新的父親比較,重復上述操作,直至該點的值大於父親。
時間復雜度分析
- 對於每個元素都要遍歷一遍,這部分是 O(n)。
- 每處理一個元素時,最多需要向根部方向交換 lognlogn 次。
因此總的時間復雜度是 O(nlogn)
基於 Siftdown 的版本 O(n)
Python版本:
import sys import collections class Solution: # @param A: Given an integer array # @return: void def siftdown(self, A, k): while k * 2 + 1 < len(A): son = k * 2 + 1 #A[i]左兒子的下標 if k * 2 + 2 < len(A) and A[son] > A[k * 2 + 2]: son = k * 2 + 2 #選擇兩個兒子中較小的一個 if A[son] >= A[k]: break temp = A[son] A[son] = A[k] A[k] = temp k = son def heapify(self, A): for i in range(len(A) - 1, -1, -1): self.siftdown(A, i)
算法思路:
- 初始選擇最接近葉子的一個父結點,與其兩個兒子中較小的一個比較,若大於兒子,則與兒子交換。
- 交換后再與新的兒子比較並交換,直至沒有兒子。
- 再選擇較淺深度的父親結點,重復上述步驟。
時間復雜度分析
這個版本的算法,乍一看也是 O(nlogn)O(nlogn), 但是我們仔細分析一下,算法從第 n/2 個數開始,倒過來進行 siftdown。也就是說,相當於從 heap 的倒數第二層開始進行 siftdown 操作,倒數第二層的節點大約有 n/4 個, 這 n/4 個數,最多 siftdown 1次就到底了,所以這一層的時間復雜度耗費是 O(n/4)O(n/4),然后倒數第三層差不多 n/8 個點,最多 siftdown 2次就到底了。所以這里的耗費是 O(n/8 * 2), 倒數第4層是 O(n/16 * 3),倒數第5層是 O(n/32 * 4) ... 因此累加所有的時間復雜度耗費為:
T(n) = O(n/4) + O(n/8 * 2) + O(n/16 * 3) ...
然后我們用 2T - T 得到:
2 * T(n) = O(n/2) + O(n/4 * 2) + O(n/8 * 3) + O(n/16 * 4) ...
T(n) = O(n/4) + O(n/8 * 2) + O(n/16 * 3) ...
2 * T(n) - T(n) = O(n/2) +O (n/4) + O(n/8) + ...
= O(n/2 + n/4 + n/8 + ... )
= O(n)
因此得到 T(n) = 2 * T(n) - T(n) = O(n)
紅黑樹(Red-black Tree)是一種平衡排序二叉樹(Balanced Binary Search Tree),在它上面進行增刪查改的平均時間復雜度都是 O(logn)O(logn),是居家旅行的常備數據結構。
Q: 在面試中考不考呢?
A: 很少考……
Q: 需不需要了解呢?
A: 需要!
Q: 了解到什么程度呢?
A: 知道它是 Balanced Binary Search Tree,知道它支持什么樣的操作,會用就行。不需要知道具體的實現原理。
紅黑樹的幾個常用操作
Java當中,紅黑樹主要是TreeSet
,位於java.util.TreeSet
,繼承自java.util.AbstractSet
,它的主要方法有:
add
,插入一個元素。remove
,刪除一個元素。clear
,刪除所有元素。contains
,查找是否包含某元素。isEmpty
,是否空樹。size
,返回元素個數。iterator
,返回迭代器。clone
,對整棵樹進行淺拷貝,即不拷貝元素本身。first
,返回最前元素。last
,返回最末元素。floor
,返回不大於給定元素的最大元素。ceiling
,返回不小於給定元素的最小元素。pollFirst
,刪除並返回首元素。pollLast
,刪除並返回末元素。
更具體的細節,請參考Java Reference。
此外,在Java當中,有一種map,用紅黑樹實現key查找,這種結構叫做TreeMap
。如果你需要一種map,並且它的key是有序的,那么強烈推薦TreeMap
。
在C++當中,紅黑樹即是默認的set
和map
,其元素也是有序的。
而通過哈系表實現的則分別是unordered_set
和unordered_map
,注意這兩種結構是在C++11
才有的。
在Python當中,默認的set和dict是用哈系表實現,沒有默認的紅黑樹。如果你想使用紅黑樹的話,可以使用rbtree
這個模塊,下載地址:https://pypi.python.org/pypi/rbtree/0.9.0