數據結構與算法(一)- 常見的數據結構及應用場景分析

算法中,往往都會涉及數據結構的選擇和使用。
本篇博文主要描述一些常用的數據結構。

如:

  • 字符串、數組
  • 隊列
  • 雙端隊列
  • 鏈表

1.字符串、數組(String & Array)

字符串轉化
數組和字符串是最基本的數據結構,在很多編程語言中都有着十分相似的性質,而圍繞着它們的算法面試題也是最多的。

很多時候,在分析字符串的過程中,我們往往要針對字符串當中的每一個字符進行分析和處理,甚至有時候我們得先把給定的字符串轉換成字符數組之後再進行分析和處理。

舉例:翻轉字符串“algorithm”。

2.隊列(Queue)

特點:和棧不同,隊列的最大特點是先進先出(FIFO),就好像按順序排隊一樣。對於隊列的數據來說,我們只允許在隊尾查看和添加數據,在隊頭查看和刪除數據。

實現:可以藉助雙鏈表來實現隊列。雙鏈表的頭指針允許在隊頭查看和刪除數據,而雙鏈表的尾指針允許我們在隊尾查看和添加數據。

應用場景:直觀來看,當我們需要按照一定的順序來處理數據,而該數據的數據量在不斷地變化的時候,則需要隊列來幫助解題。在算法面試題當中,廣度優先搜索(Breadth-First Search)是運用隊列最多的地方,我們將在第 06 課時中詳細介紹。

python模擬實現:

DEFAULT_CAPACITY = 10


class Empty(Exception):
    pass


class ArrayQueue(object):

    def __init__(self):
        self._data = [None] * DEFAULT_CAPACITY
        self._size = 0
        self._front = 0

    def __len__(self):
        return self._size

    def is_empty(self):
        """Return True if the queue is empty."""
        return self._size == 0

    def first(self):
        """Return the element at the front of the queue."""
        if self.is_empty():
            raise Empty('Queue is empty.')
        return self._data[self._front]

    def dequeue(self):
        """Remove and return the first element of the queue."""
        if self.is_empty():
            raise Empty('Queue is empty.')
        answer = self._data[self._front]
        self._data[self._front] = None
        self._front = (self._front + 1) % len(self._data)
        self._size -= 1
        return answer

    def enqueue(self, e):
        """Add an element to the back of the queue."""
        if self._size == len(self._data):
            self._resize(2 * len(self._data))
        avail = (self._front + self._size) % len(self._data)
        self._data[avail] = e
        self._size += 1

    def _resize(self, cap):
        """Resize a new list of capacity >= len(self)."""
        old = self._data
        self._data = [None] * cap
        walk = self._front
        for k in range(self._size):
            self._data[k] = old[walk]
            walk = (1 + walk) % len(old)
        self._front = 0

3.雙端隊列(Deque)

特點:雙端隊列和普通隊列最大的不同在於,它允許我們在隊列的頭尾兩端都能在 O(1) 的時間內進行數據的查看、添加和刪除。

實現:與隊列相似,我們可以利用一個雙鏈表實現雙端隊列。

應用場景:雙端隊列最常用的地方就是實現一個長度動態變化的窗口或者連續區間,而動態窗口這種數據結構在很多題目裏都有運用。

4.鏈表(LinkedList)

單鏈表:鏈表中的每個元素實際上是一個單獨的對象,而所有對象都通過每個元素中的引用字段鏈接在一起。

雙鏈表:與單鏈表不同的是,雙鏈表的每個結點中都含有兩個引用字段。

class ListNode(object):
    __slots__ = ('val', 'next')

    def __init__(self, x):
        self.val = x
        self.next = None

4.1.鏈表的優缺點

鏈表的優點如下:

  • 鏈表能靈活地分配內存空間;
  • 能在 O(1) 時間內刪除或者添加元素,前提是該元素的前一個元素已知,當然也取決於是單鏈表還是雙鏈表,在雙鏈表中,如果已知該元素的後一個元素,同樣可以在 O(1) 時間內刪除或者添加該元素。

鏈表的缺點是:

  • 不像數組能通過下標迅速讀取元素,每次都要從鏈表頭開始一個一個讀取;
  • 查詢第 k 個元素需要 O(k) 時間。

4.2.應用場景

如果要解決的問題裏面需要很多快速查詢,鏈表可能並不適合;

如果遇到的問題中,數據的元素個數不確定,而且需要經常進行數據的添加和刪除,那麼鏈表會比較合適。

而如果數據元素大小確定,刪除插入的操作並不多,那麼數組可能更適合。

5.棧(Stack)

特點:棧的最大特點就是後進先出(LIFO)。對於棧中的數據來說,所有操作都是在棧的頂部完成的,只可以查看棧頂部的元素,只能夠向棧的頂部壓⼊數據,也只能從棧的頂部彈出數據。

實現:利用一個單鏈表來實現棧的數據結構。而且,因爲我們都只針對棧頂元素進行操作,所以借用單鏈表的頭就能讓所有棧的操作在 O(1) 的時間內完成。

應用場景:在解決某個問題的時候,只要求關心最近一次的操作,並且在操作完成了之後,需要向前查找到更前一次的操作。

如果打算用一個數組外加一個指針來實現相似的效果,那麼,一旦數組的長度發生了改變,哪怕只是在最後添加一個新的元素,時間複雜度都不再是 O(1),而且,空間複雜度也得不到優化。

注意:棧是許多 LeetCode 中等難度偏上的題目裏面經常需要用到的數據結構,掌握好它是十分必要的。

python模擬棧的實現:

class ArrayStack(object):

    def __init__(self):
        self._data = []
        self._min_data = []
        self._max_data = []
        self.minVal = None
        self.maxVal = None

    def __len__(self):
        return len(self._data)

    def _set_min_value(self, val):
        """設置最小值"""
        if not self._min_data:
            self._min_data.append(val)
            self.minVal = val
        else:
            if val <= self.minVal:
                self._min_data.append(val)
                self.minVal = val
        print('min-list: ', self._min_data, end=' ')

    def _set_max_value(self, val):
        """設置最大值"""
        if not self._max_data:
            self._max_data.append(val)
            self.maxVal = val
        else:
            if val >= self.maxVal:
                self._max_data.append(val)
                self.maxVal = val
        print('max-list: ', self._max_data)

    def _update_min_value(self, val):
        """更新最小值"""
        if val == self.minVal:
            self._min_data.pop()
            if not self._min_data:
                self.minVal = None
            else:
                self.minVal = self._min_data[-1]
        else:
            if val in self._min_data:
                self._min_data.remove(val)
        print('min-list: ', self._min_data, end=' ')

    def _update_max_value(self, val):
        """更新最大值"""
        if val == self.maxVal:
            self._max_data.pop()
            if not self._max_data:
                self.maxVal = None
            else:
                self.maxVal = self._max_data[-1]
        else:
            if val in self._max_data:
                self._max_data.remove(val)
        print('max-list: ', self._max_data)

    def is_empty(self):
        """Return True if the stack is empty."""
        return len(self._data) == 0

    def pop(self):
        """Remove and return the element from the top of the stack."""
        if self.is_empty():
            raise Empty('Stack is empty.')

        pop_val = self._data.pop()

        self._update_min_value(pop_val)
        self._update_max_value(pop_val)

        return pop_val

    def push(self, e):
        """Add an element e to the top of the stack."""
        self._data.append(e)
        self._set_min_value(e)
        self._set_max_value(e)

    def top(self):
        """Return the element at the top of the stack."""
        if self.is_empty():
            raise Empty('Stack is empty.')
        return self._data[-1]

6.樹(Tree)

樹的結構十分直觀,而樹的很多概念定義都有一個相同的特點:遞歸,也就是說,一棵樹要滿足某種性質,往往要求每個節點都必須滿足。例如,在定義一棵二叉搜索樹時,每個節點也都必須是一棵二叉搜索樹。

正因爲樹有這樣的性質,大部分關於樹的面試題都與遞歸有關,換句話說,面試官希望通過一道關於樹的問題來考察你對於遞歸算法掌握的熟練程度。

樹的形狀
在面試中常考的樹的形狀有:普通二叉樹、平衡二叉樹、完全二叉樹、二叉搜索樹、四叉樹(Quadtree)、多叉樹(N-ary Tree)。

對於一些特殊的樹,例如紅黑樹(Red-Black Tree)、自平衡二叉搜索樹(AVL Tree),一般在面試中不會被問到,除非你所涉及的研究領域跟它們相關或者你十分感興趣,否則不需要特別着重準備。

python模擬樹的實現:

class Tree(object):
    """創建樹"""

    default_chars = [chr(c) for c in range(65, 91)]

    def __init__(self, seq=None):
        """
        初始化
        :param seq: 二叉樹元素序列
        """
        seq = self.default_chars if seq is None else seq
        self.chars = seq

    def create(self):
        """執行創建"""
        tree = self._recursive_create_node(self.chars)

        return tree

    def _recursive_create_node(self, seq):
        """遞歸創建節點"""
        n = len(seq)
        if n == 0:
            return None
        i = n // 2
        return Node(seq[i], self._recursive_create_node(seq[:i]), self._recursive_create_node(seq[i + 1:]))
    

樹的遍歷

  1. 前序遍歷(Preorder Traversal)

方法:先訪問根節點,然後訪問左子樹,最後訪問右子樹。在訪問左、右子樹的時候,同樣,先訪問子樹的根節點,再訪問子樹根節點的左子樹和右子樹,這是一個不斷遞歸的過程。

應用場景:運用最多的場合包括在樹裏進行搜索以及創建一棵新的樹。

python實現前序遍歷:

# 遞歸實現
def pre_order(tree, lst=[]):
    """前序遍歷: 根->左->右"""
    if tree is None:
        return
    # print(tree.data, end='->')
    lst.append(tree.data)
    self.pre_order(tree.left, lst)
    self.pre_order(tree.right, lst)
    return lst
  1. 中序遍歷(Inorder Traversal)

方法:先訪問左子樹,然後訪問根節點,最後訪問右子樹,在訪問左、右子樹的時候,同樣,先訪問子樹的左邊,再訪問子樹的根節點,最後再訪問子樹的右邊。

應用場景:最常見的是二叉搜素樹,由於二叉搜索樹的性質就是左孩子小於根節點,根節點小於右孩子,對二叉搜索樹進行中序遍歷的時候,被訪問到的節點大小是按順序進行的。

python實現中序遍歷:

# 遞歸實現
def mid_order(tree, lst=[]):
    """中序遍歷: 左->根->右"""
    if tree is None:
        return
    self.mid_order(tree.left, lst)
    # print(tree.data, end='->')
    lst.append(tree.data)
    self.mid_order(tree.right, lst)
    return lst
  1. 後序遍歷(Postorder Traversal)

方法:先訪問左子樹,然後訪問右子樹,最後訪問根節點。

應用場景:在對某個節點進行分析的時候,需要來自左子樹和右子樹的信息。收集信息的操作是從樹的底部不斷地往上進行,好比你在修剪一棵樹的葉子,修剪的方法是從外面不斷地往根部將葉子一片片地修剪掉。

python實現後序遍歷:

# 遞歸實現
def post_order(tree, lst=[]):
    """後序遍歷: 左->右->根"""
    if tree is None:
        return
    self.post_order(tree.left, lst)
    self.post_order(tree.right, lst)
    # print(tree.data, end='->')
    lst.append(tree.data)
    return lst
  1. 廣度遍歷(Breadth traversal)

python實現:

def level_order(tree):
    """廣度遍歷"""
    lst = list()
    if tree is None:
        return lst

    q = list()
    q.append(tree)

    while len(q) > 0:
        node = q.pop(0)
        # print(node.data, end='->')
        lst.append(node.data)

        if node.left:
            q.append(node.left)  # 左子節點入隊
        if node.right:
            q.append(node.right)  # 右子節點入隊

    return lst

內容來源:拉勾網(數據結構與算法)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章