数组/链表/栈/队列/树/图 等数据结构的优缺点及应用场景

数组、字符串(Array & String)

数组的优点在于:

  • 构建非常简单
  • 能在 O(1) 的时间里根据数组的下标(index)查询某个元素

而数组的缺点在于:

  • 构建时必须分配一段连续的空间
  • 查询某个元素是否存在时需要遍历整个数组,耗费 O(n) 的时间(其中,n 是元素的个数)
  • 删除和添加某个元素时,同样需要耗费 O(n) 的时间

 链表(LinkedList)

单链表:链表中的每个元素实际上是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
双链表:与单链表不同的是,双链表的每个结点中都含有两个引用字段。

由一系列节点组成的元素集合,每个节点包含数据域item和下一个节点的指针next,通过节点相互连接,最终串联成一个链表。

class Node(object):
    def __init__(self,item):
        self.item = item
        self.next = None
a = [1,2,3]
b = Node(a)
c = Node(4)
b.next = c
print(b.next.item)

 

链表的优点如下:

  • 链表能灵活地分配内存空间;
  • 能在 O(1) 时间内删除或者添加元素,前提是该元素的前一个元素已知,当然也取决于是单链表还是双链表,在双链表中,如果已知该元素的后一个元素,同样可以在 O(1) 时间内删除或者添加该元素。

链表的缺点是:

  • 不像数组能通过下标迅速读取元素,每次都要从链表头开始一个一个读取;
  • 查询第 k 个元素需要 O(k) 时间。

应用场景:如果要解决的问题里面需要很多快速查询,链表可能并不适合;如果遇到的问题中,数据的元素个数不确定,而且需要经常进行数据的添加和删除,那么链表会比较合适。而如果数据元素大小确定,删除插入的操作并不多,那么数组可能更适合。

栈(Stack)

特点:栈的最大特点就是后进先出(LIFO)。对于栈中的数据来说,所有操作都是在栈的顶部完成的,只可以查看栈顶部的元素,只能够向栈的顶部压⼊数据,也只能从栈的顶部弹出数据。
实现:利用一个单链表来实现栈的数据结构。而且,因为我们都只针对栈顶元素进行操作,所以借用单链表的头就能让所有栈的操作在 O(1) 的时间内完成。
应用场景:在解决某个问题的时候,只要求关心最近一次的操作,并且在操作完成了之后,需要向前查找到更前一次的操作.

队列(Queue)

特点:和栈不同,队列的最大特点是先进先出(FIFO),就好像按顺序排队一样。对于队列的数据来说,我们只允许在队尾查看和添加数据,在队头查看和删除数据。
实现:可以借助双链表来实现队列。双链表的头指针允许在队头查看和删除数据,而双链表的尾指针允许我们在队尾查看和添加数据。
应用场景:直观来看,当我们需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来帮助解题。在算法面试题当中,广度优先搜索(Breadth-First Search)是运用队列最多的地方。

双端队列(Deque)

特点:双端队列和普通队列最大的不同在于,它允许我们在队列的头尾两端都能在 O(1) 的时间内进行数据的查看、添加和删除。
实现:与队列相似,我们可以利用一个双链表实现双端队列。
应用场景:双端队列最常用的地方就是实现一个长度动态变化的窗口或者连续区间,而动态窗口这种数据结构在很多题目里都有运用。

树(Tree)

树的结构十分直观,而树的很多概念定义都有一个相同的特点:递归,也就是说,一棵树要满足某种性质,往往要求每个节点都必须满足。例如,在定义一棵二叉搜索树时,每个节点也都必须是一棵二叉搜索树。
正因为树有这样的性质,大部分关于树的面试题都与递归有关,换句话说,面试官希望通过一道关于树的问题来考察你对于递归算法掌握的熟练程度。
树的形状

在面试中常考的树的形状有:普通二叉树、平衡二叉树、完全二叉树、二叉搜索树、四叉树(Quadtree)、多叉树(N-ary Tree)。
对于一些特殊的树,例如红黑树(Red-Black Tree)、自平衡二叉搜索树(AVL Tree),一般在面试中不会被问到,除非你所涉及的研究领域跟它们相关或者你十分感兴趣,否则不需要特别着重准备。
关于树的考题,无非就是要考查树的遍历以及序列化(serialization)

树的遍历

1. 前序遍历(Preorder Traversal)

方法:先访问根节点,然后访问左子树,最后访问右子树。在访问左、右子树的时候,同样,先访问子树的根节点,再访问子树根节点的左子树和右子树,这是一个不断递归的过程。

应用场景:运用最多的场合包括在树里进行搜索以及创建一棵新的树。
 
2. 中序遍历(Inorder Traversal)

方法:先访问左子树,然后访问根节点,最后访问右子树,在访问左、右子树的时候,同样,先访问子树的左边,再访问子树的根节点,最后再访问子树的右边。

应用场景:最常见的是二叉搜索树,由于二叉搜索树的性质就是左孩子小于根节点,根节点小于右孩子,对二叉搜索树进行中序遍历的时候,被访问到的节点大小是按顺序进行的。
 
3. 后序遍历(Postorder Traversal)

方法:先访问左子树,然后访问右子树,最后访问根节点。

应用场景:在对某个节点进行分析的时候,需要来自左子树和右子树的信息。收集信息的操作是从树的底部不断地往上进行,好比你在修剪一棵树的叶子,修剪的方法是从外面不断地往根部将叶子一片片地修剪掉。

注意:
掌握好这三种遍历的递归写法和非递归写法是非常重要的,懂得分析各种写法的时间复杂度和空间复杂度同样重要。

树这个数据结构都是最应该花时间学习的,既能证明你对递归有很好的认识,又能帮助你学习图论,尤其是二叉搜索树(BST)。

 

#二叉树

class BiTreeNode:
    def __init__(self,data):
        self.data = data
        self.lchild = None
        self.rchild = None

a = BiTreeNode('A')
b = BiTreeNode('B')
c = BiTreeNode('C')
d = BiTreeNode('D')
e = BiTreeNode('E')
f = BiTreeNode('F')
g = BiTreeNode('G')

e.lchild = a
e.rchild = g
a.rchild = c
c.lchild = b
c.rchild = d
g.rchild = f
root = e


#前序遍历
def pre_order(root):
    if root:
        print(root.data,end = ',')
        pre_order(root.lchild)
        pre_order(root.rchild)
#中序遍历
def in_order(root):
    if root:
        in_order(root.lchild)
        print(root.data,end =',')
        in_order(root.rchild)
#后序遍历
def post_order(root):
    if root:
        in_order(root.lchild)
        in_order(root.rchild)
        print(root.data,end = ',')

in_order(root)

 

优先队列(Priority Queue)

特点

能保证每次取出的元素都是队列中优先级别最高的。优先级别可以是自定义的,例如,数据的数值越大,优先级越高;或者数据的数值越小,优先级越高。优先级别甚至可以通过各种复杂的计算得到。
应用场景

从一堆杂乱无章的数据当中按照一定的顺序(或者优先级)逐步地筛选出部分乃至全部的数据。

优先队列的本质是一个二叉堆结构。堆在英文里叫 Binary Heap,它是利用一个数组结构来实现的完全二叉树。换句话说,优先队列的本质是一个数组,数组里的每个元素既有可能是其他元素的父节点,也有可能是其他元素的子节点,而且,每个父节点只能有两个子节点,很像一棵二叉树的结构。

牢记下面优先队列有三个重要的性质。

  • 数组里的第一个元素 array[0] 拥有最高的优先级别。
  • 给定一个下标 i,那么对于元素 array[i] 而言:
  1. 它的父节点所对应的元素下标是 (i-1)/2
  2. 它的左孩子所对应的元素下标是 2×i + 1
  3. 它的右孩子所对应的元素下标是 2×i + 2
  • 数组里每个元素的优先级别都要高于它两个孩子的优先级别。

优先队列最基本的操作有两个。

  • 向上筛选(sift up / bubble up)

当有新的数据加入到优先队列中,新的数据首先被放置在二叉堆的底部。

不断进行向上筛选的操作,即如果发现该数据的优先级别比父节点的优先级别还要高,那么就和父节点的元素相互交换,再接着往上进行比较,直到无法再继续交换为止。

  • 向下筛选(sift down / bubble down)

当堆顶的元素被取出时,要更新堆顶的元素来作为下一次按照优先级顺序被取出的对象,需要将堆底部的元素放置到堆顶,然后不断地对它执行向下筛选的操作。

将该元素和它的两个孩子节点对比优先级,如果优先级最高的是其中一个孩子,就将该元素和那个孩子进行交换,然后反复进行下去,直到无法继续交换为止。

图(Graph)

  • 阶(Order)、度:出度(Out-Degree)、入度(In-Degree)
  • 树(Tree)、森林(Forest)、环(Loop)
  • 有向图(Directed Graph)、无向图(Undirected Graph)、完全有向图、完全无向图
  • 连通图(Connected Graph)、连通分量(Connected Component)
  • 存储和表达方式:邻接矩阵(Adjacency Matrix)、邻接链表(Adjacency List)

常用图方法

  • 图的存储和表达方式:邻接矩阵(Adjacency Matrix)、邻接链表(Adjacency List)
  • 图的遍历:深度优先、广度优先
  • 二部图的检测(Bipartite)、树的检测、环的检测:有向图、无向图
  • 拓扑排序
  • 联合-查找算法(Union-Find)
  • 最短路径:Dijkstra、Bellman-Ford

环的检测、二部图的检测、树的检测以及拓扑排序都是基于图的遍历,尤其是深度优先方式的遍历。而遍历可以在邻接矩阵或者邻接链表上进行,所以掌握好图的遍历是重中之重!因为它是所有其他图论算法的基础。

前缀树(Trie)

前缀树被广泛地运用在字典查找当中,也被称为字典树。
举例:给定一系列字符串,这些字符串构成了一种字典,要求你在这个字典当中找出所有以“ABC”开头的字符串。

如果用前缀树头帮助对字典的存储进行优化,那么可以把搜索的时间复杂度下降为 O(M),其中 M 表示字典里最长的那个单词的字符个数,在很多情况下,字典里的单词个数 N 是远远大于 M 的。因此,前缀树在这种场合中是非常高效的。
经典应用

网站上的搜索框会罗列出以搜索文字作为开头的相关搜索信息,这里运用了前缀树进行后端的快速检索。

汉字拼音输入法的联想输出功能也运用了前缀树。

 

posted @ 2020-06-29 17:03  Christbao  阅读(3794)  评论(1编辑  收藏  举报