棧 隊列 hash表 堆 算法模板和相關題目


 

什么是棧(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 來進行非遞歸的二叉樹遍歷。

這里我們來看看棧在面試中的其他一些考點和考題:

  1. 如果自己實現一個棧?
  2. 如何用兩個隊列實現一個棧?
  3. 用一個數組如何實現三個棧?

 

隊列

定義

  1. 隊列為一種先進先出的線性表
  2. 只允許在表的一端進行入隊,在另一端進行出隊操作。在隊列中,允許插入的一端叫隊尾,允許刪除的一端叫隊頭,即入隊只能從隊尾入,出隊只能從隊頭出

思路

  1. 需要兩個節點,一個頭部節點,也就是dummy節點,它是在加入的第一個元素的前面,也就是dummy.next=第一個元素,這樣做是為了方便我們刪除元素,還有一個尾部節點,也就是tail節點,表示的是最后一個元素的節點
  2. 初始時,tail節點跟dummy節點重合
  3. 當我們要加入一個元素時,也就是從隊尾中加入一個元素,只需要新建一個值為val的node節點,然后tail.next=node,再移動tail節點到tail.next
  4. 當我們需要刪除隊頭元素時,只需要將dummy.next變為dummy.next.next,這樣就刪掉了第一個元素,這里需要注意的是,如果刪掉的是隊列中唯一的一個元素,那么需要將tail重新與dummy節點重合
  5. 當我們需要得到隊頭元素而不刪除這個元素時,只需要獲得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 的隊列考題考點做一個匯總:

  1. 如何用鏈表實現隊列?
  2. 如何用兩個棧實現一個隊列?
  3. 什么是循環數組,如何用循環數組實現隊列?

什么是循環數組,如何用循環數組實現隊列?

什么是循環數組

Circular array = a data structure that used a array as if it were connected end-to-end

可以圖示為:

circular array

如何實現隊列

  1. 我們需要知道隊列的入隊操作是只在隊尾進行的,相對的出隊操作是只在隊頭進行的,所以需要兩個變量front與rear分別來指向隊頭與隊尾
  2. 由於是循環隊列,我們在增加元素時,如果此時 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. 數據流滑動窗口平均值

中文
English

給出一串整數流和窗口大小,計算滑動窗口中所有整數的平均值。

樣例

樣例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緩存策略

中文
English

為最近最少使用(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

中文
English

我們需要實現一個叫 DataStream 的數據結構。並且這里有 個方法需要實現:

  1. void add(number) // 加一個新的數
  2. 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)

中文
English

設計一個數據結構實現在平均 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) - 允許重復

中文
English

設計一個數據結構,需要支持以下操作,且平均時間復雜度為 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)是面試中非常常見的數據結構。它的主要考點有兩個:

  1. 是否會靈活的使用哈希表解決問題
  2. 是否熟練掌握哈希表的基本原理

這一小節中,我們將介紹一下哈希表原理中的幾個重要的知識點:

  1. 哈希表的工作原理
  2. 為什么 hash 上各種操作的時間復雜度不能單純的認為是 O(1) 的
  3. 哈希函數(Hash Function)該如何實現
  4. 哈希沖突(Collision)該如何解決
  5. 如何讓哈希表可以不斷擴容?

在數據結構中,哈希函數是用來將一個字符串(或任何其他類型)轉化為小於哈希表大小且大於等於零的整數。一個好的哈希函數可以盡可能少地產生沖突。一種廣泛使用的哈希函數算法是使用數值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 經過哈希函數的計算后,得到了兩個相同的值。解決沖突的方法,主要有兩種:

  1. 開散列法(Open Hashing)。是指哈希表所基於的數組中,每個位置是一個 Linked List 的頭結點。這樣沖突的 <key, value> 二元組,就都放在同一個鏈表中。
  2. 閉散列法(Closed Hashing)。是指在發生沖突的時候,后來的元素,往下一個位置去找空位。

哈希表容量的大小在一開始是不確定的。如果哈希表存儲的元素太多(如超過容量的十分之一),我們應該將哈希表容量擴大一倍,並將所有的哈希值重新安排。假設你有如下一哈希表:

size=3capacity=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=3capacity=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. 子數組之和

中文
English

給定一個整數數組,找到和為零的子數組。你的代碼應該返回滿足要求的子數組的起始位置和結束位置

樣例

樣例 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. 復制帶隨機指針的鏈表

中文
English

給出一個鏈表,每個節點包含一個額外增加的隨機指針可以指向鏈表中的任何節點或空的節點。

返回一個深拷貝的鏈表。 

挑戰

可否使用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. 最長連續序列

中文
English

給定一個未排序的整數數組,找出最長連續序列的長度。

樣例

樣例 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) 

算法思路:

  1. 對於每個元素A[i],比較A[i]和它的父親結點的大小,如果小於父親結點,則與父親結點交換。
  2. 交換后再和新的父親比較,重復上述操作,直至該點的值大於父親。

時間復雜度分析

  1. 對於每個元素都要遍歷一遍,這部分是 O(n)
  2. 每處理一個元素時,最多需要向根部方向交換 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) 

算法思路:

  1. 初始選擇最接近葉子的一個父結點,與其兩個兒子中較小的一個比較,若大於兒子,則與兒子交換。
  2. 交換后再與新的兒子比較並交換,直至沒有兒子。
  3. 再選擇較淺深度的父親結點,重復上述步驟。

時間復雜度分析

這個版本的算法,乍一看也是 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++當中,紅黑樹即是默認的setmap,其元素也是有序的。
而通過哈系表實現的則分別是unordered_setunordered_map,注意這兩種結構是在C++11才有的。
在Python當中,默認的set和dict是用哈系表實現,沒有默認的紅黑樹。如果你想使用紅黑樹的話,可以使用rbtree這個模塊,下載地址:https://pypi.python.org/pypi/rbtree/0.9.0

Merge K Sorted Lists 多路歸並算法的三種實現方式

外排序與K路歸並算法

介紹

外排序算法(External Sorting),是指在內存不夠的情況下,如何對存儲在一個或者多個大文件中的數據進行排序的算法。外排序算法通常是解決一些大數據處理問題的第一個步驟,或者是面試官所會考察的算法基本功。外排序算法是海量數據處理算法中十分重要的一塊。
在學習這類大數據算法時,經常要考慮到內存、緩存、准確度等因素,這和我們之前見到的算法都略有差別。

基本步驟

外排序算法分為兩個基本步驟:

  1. 將大文件切分為若干個個小文件,並分別使用內存排好序
  2. 使用K路歸並算法(k-way merge)將若干個排好序的小文件合並到一個大文件中

第一步:文件拆分

根據內存的大小,盡可能多的分批次的將數據 Load 到內存中,並使用系統自帶的內存排序函數(或者自己寫個快速排序算法),將其排好序,並輸出到一個個小文件中。比如一個文件有1T,內存有1G,那么我們就這個大文件中的內容按照 1G 的大小,分批次的導入內存,排序之后輸出得到 1024 個 1G 的小文件。

第二步:K路歸並算法

K路歸並算法使用的是數據結構堆(Heap)來完成的,使用 Java 或者 C++ 的同學可以直接用語言自帶的 PriorityQueue(C++中叫priority_queue)來代替。

我們將 K 個文件中的第一個元素加入到堆里,假設數據是從小到大排序的話,那么這個堆是一個最小堆(Min Heap)。每次從堆中選出最小的元素,輸出到目標結果文件中,然后如果這個元素來自第 x 個文件,則從第 x 個文件中繼續讀入一個新的數進來放到堆里,並重復上述操作,直到所有元素都被輸出到目標結果文件中。

Follow up: 一個個從文件中讀入數據,一個個輸出到目標文件中操作很慢,如何優化?

如果我們每個文件只讀入1個元素並放入堆里的話,總共只用到了 1024 個元素,這很小,沒有充分的利用好內存。另外,單個讀入和單個輸出的方式也不是磁盤的高效使用方式。因此我們可以為輸入和輸出都分別加入一個緩沖(Buffer)。假如一個元素有10個字節大小的話,1024 個元素一共 10K,1G的內存可以支持約 100K 組這樣的數據,那么我們就為每個文件設置一個 100K 大小的 Buffer,每次需要從某個文件中讀數據,都將這個 Buffer 裝滿。當然 Buffer 中的數據都用完的時候,再批量的從文件中讀入。輸出同理,設置一個 Buffer 來避免單個輸出帶來的效率緩慢。

相關練習

Lintcode相關練習
合並K個有序數組
合並K個有序鏈表

面試相關問題
面試題:合並 K 個排好序的大文件
面試題:求兩個超大文件中 URLs 的交集

更多海量數據算法相關知識可參見
九章算法——海量數據處理算法與面試題全集

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM