算法中,往往都会涉及数据结构的选择和使用。
本篇博文主要描述一些常用的数据结构。
如:
- 字符串、数组
- 队列
- 双端队列
- 链表
- 栈
- 树
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:]))
树的遍历
- 前序遍历(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
- 中序遍历(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
- 后序遍历(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
- 广度遍历(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
内容来源:拉勾网(数据结构与算法)