当解决实际编码问题时,每个人都在追求执行时和资源消耗方面的高效率。
选择当前方案中最适合的数据结构,会提升程序性能并且减少执行时间。因此很多大公司在面试的时候都非常注重求职者对数据结构的理解。
本文包含:
- 什么是数据结构
- 数组/列表 Array/List
- 队列 Queue
- 栈 Stack
- 链表 Linked list
- 循环链表 Circular linked list
- 树 Tree
- 图 Graph
- 哈希表 Hashable Table
什么是数据结构?
数据结构是一种存储和组织数据,使它更容易修改、浏览和访问的代码结构。数据结构决定了数据是如何聚集的、我们能使用的功能和数据之间的关系。
数据结构几乎应用在所有的计算机科学和编程领域,从操作系统到 Web前后端开发再到机器学习。
数据结构可以帮助我们
- 管理和使用大的数据集
- 从数据库中快速搜索指定信息
- 在数据点之间建立清晰的层次或关系连接
- 简化并加速数据处理
数据结构是高效解决实际问题的至关重要的一部分,是一种经过反复验证和优化的工具,可以给你提供一种简易的框架来组织你的程序。这样你解决问题的时候,不需要每次重新造轮子。
每种数据结构都有一个与之最匹配的任务或使用情境。Python中有四种内置的数据结构,列表、字典、元组和集合。这些内置结构有一些默认方法,并做了些底层的优化,使他们更好用。
大多数 Python 中的数据结构都修正了这些内置结构的格式或使用它们作为它的基础。
- 列表:类数组结构,保存一个相同类型的可变对象的集合
- 元组:不可变列表,元素不能更改
- 集合:无序集合,元素内部未索引,且不可重复
- 字典:类似哈希表,key/value 键值对集合,键值唯一且为不可变对象
下面我们看看这些结构是如何创造一些高级数据结构的。
数组/列表 Array/List
Python 中没有内置的 array 类型,但可以使用 List 来实现。数组是一些值的集合,这些值具有相同类型,并保存在同一名称下。
列表中的每个值都叫一个元素,同时具有一个索引,代表它的位置。你可以通过使用列表名和索引值来访问特定的元素,也可以通过 len()
方法获取列表的长度。
不像 Java等静态语言,Python中的列表会自动放大或缩小,当元素增加/减少的时候。
比如,我们可以通过 append()
方法在一个已存在的列表末尾增加一个额外的元素而不需要声明一个新列表。
这使得Python列表更好使用,能动态适应。
cars = ["Toyota", "Tesla", "Hyundai"]
print(len(cars))
cars.append("Honda")
cars.pop(1)
for x in cars:
print(x)
优点
- 创建和使用数据序列更简单
- 自动缩放以满足不断变化的尺寸要求
- 可以用作创建更复杂的数据结构
缺点
- 没有针对科学数据方面的优化(不像 NumPy中的列表)
- 只能控制列表的末尾
应用场景
- 分享相关联值或对象的存储
- 你将遍历的数据集
- 数据结构的集合,比如一个元组的列表
队列 Queue
队列是一种线性结构,存储数据时遵循”先进先出”(FIFO)的次序。不像列表,你不能通过索引值访问元素,只能获得最先进去的元素。这个非常适合于一些对次序敏感的任务,比如线上订单处理、语言信箱存储。
我们可以通过list的 append 和 pop 方法实现一个队列。但这样效率不高,因为list在头部增加一个元素的时候需要移动所有的元素。
使用 collections 模块的deque类是个更好的选择。deque优化了append和pop的操作,很容易实现一个双端队列(double-ended queue),可以通过popleft和popright方法访问队列的头尾两端。
from collections import deque
# Initializing a queue
q = deque()
# Adding elements to a queue
q.append('a')
q.append('b')
q.append('c')
print("Initial queue")
print(q)
# Removing elements from a queue
print("
Elements dequeued from the queue")
print(q.popleft())
print(q.popleft())
print(q.popleft())
print("
Queue after removing elements")
print(q)
# Uncommenting q.popleft()
# will raise an IndexError
# as queue is now empty
优点
- 自动按时间顺序排列数据
- 自动缩放以满足尺寸要求
- 使用deque类在时间上的高效
缺点
- 只能在头尾访问数据
应用场景
- 对共享资源(如打印机或CPU内核)的操作
- 作为批处理系统的临时存储
- 为同等重要的任务提供简单的默认顺序
栈 Stack
栈是一种“后进先出”(LIFO)的队列。最后入栈的元素被认为是在栈顶,是唯一可以访问的元素,想访问中间的元素,必须首先删除足够的元素,以确保指定的元素在栈顶。
许多开发者把栈想象成一堆碟子,你只能在碟子堆顶部添加或移除碟子,如果想把碟子放在底部则必须移除整个栈。
添加元素用 push,删除元素用 pop。可以使用Python内置的list来实现栈,实现时分别使用 append 和 pop 来实现 push和 pop操作。
stack = []
# append() function to push
# element in the stack
stack.append('a')
stack.append('b')
stack.append('c')
print('Initial stack')
print(stack)
# pop() function to pop
# element from stack in
# LIFO order
print('
Elements popped from stack:')
print(stack.pop())
print(stack.pop())
print(stack.pop())
print('
Stack after elements are popped:')
print(stack)
# uncommenting print(stack.pop())
# will cause an IndexError
# as the stack is now empty
优点
- 提供了LIFO的数据管理
- 自动缩放和对象清理
- 简单可靠的数据存储系统
缺点
- 栈内存有限
- 栈中对象太多会导致栈溢出错误
应用场景
- 创建高反应性(highly reactive)系统
- 内存管理系统使用栈处理首先响应最近的请求
- 有助于括号匹配等问题
链表 Linked List
链表是一个连续的数据集合,它使用每个数据节点上的关系指针链接到列表中的下一个节点。
不同于数组,链表中没有对象所在的位置,而拥有关系位置,根据包围他们的节点确定。
链表中的第一个节点被称为头结点,最后一个称为尾结点,它有一个空指针。
根据节点是只有一个指向下一个节点的指针还是同时也拥有指向上一个节点的第二个指针,链表可分为单向链表和双向链表。
可以把链表想象成一个链条⛓,单个连接都只与相邻的节点相连,所有的连接一起形成一个更大的结构。
Python没有一个内置的链表实现方式,因此需要你实现一个 Node类来存放节点值和一个或多个指针。
class Node:
def __init__(self, dataval=None):
self.dataval = dataval
self.nextval = None
class SLinkedList:
def __init__(self):
self.headval = None
list1 = SLinkedList()
list1.headval = Node("Mon")
e2 = Node("Tue")
e3 = Node("Wed")
# Link first Node to second node
list1.headval.nextval = e2
# Link second Node to third node
e2.nextval = e3
链表主要用于创建高级数据结构,如图形和树,或者用于需要在结构中频繁添加/删除元素的任务。
优点
- 高效地添加和删除新元素
- 比数组更易于重组
- 可用作高级数据结构(如图形或树)的起点
缺点
- 数据节点存储的指针增加了内存的开销
- 必须始终从Head节点遍历链表才能找到特定元素
应用场景
- 作为高级数据结构的组成部分
- 用于需要频繁添加/删除元素的方案
循环链表 Circular linked list
标准链表的主要缺点是你始终需要从头节点开始,循环链表通过将尾结点的空指针null替换成头结点,从而解决了这个问题,链表遍历的时候程序会一直跟踪指针,直到它回到它开始的节点。
这样设置的优势是可以从任何节点开始遍历整个链表。它还允许您通过设置结构中所需的循环数,将链表用作可循环结构。循环列表非常适合用于需要长时间循环的处理中,比如操作系统中的CPU分配。
优点
- 可以从任何节点遍历链表
- 使链表更适合循环结构
缺点
- 没有null标记,找到头尾节点更加困难
应用场景
- 定期循环解决方案,如CPU调度
树 Tree
树是另一种基于关系的数据结构,专门用于表示层次关系。同链表一样,树也存在于包含节点值和一个或多个指针的节点对象中。
每个树都包含一个根节点,其他所有节点都从中产生。根节点包含直接跟它相连的节点的指针,这些节点被称为子节点。子节点也可以用于自己的子节点。二叉树的子节点数量不得超过2个。
同一个等级的节点称为同级节点,没有子节点的节点称为叶子节点。
二叉树的最常见的应用是二叉搜索树。二叉搜索树长于搜索大的数据集,时间复杂度取决于树的深度而非节点数量。
二叉搜索树的四个规则
- 左子树只包含元素小于根节点的节点
- 右子树只包含元素大于根节点的节点
- 左右子树必须也是二叉搜索树
- 没有重复的节点,也就是每个节点的值都不相同
class Node:
def __init__(self, data):
self.left = None
self.right = None
self.data = data
def insert(self, data):
# Compare the new value with the parent node
if self.data:
if data < self.data:
if self.left is None:
self.left = Node(data)
else:
self.left.insert(data)
elif data > self.data:
if self.right is None:
self.right = Node(data)
else:
self.right.insert(data)
else:
self.data = data
# Print the tree
def PrintTree(self):
if self.left:
self.left.PrintTree()
print( self.data),
if self.right:
self.right.PrintTree()
# Use the insert method to add nodes
root = Node(12)
root.insert(6)
root.insert(14)
root.insert(3)
root.PrintTree()
优点
- 适合表示层级关系
- 动态大小
- 快速增加和删除
- 在二叉搜索树中,插入的节点会立即排序
- 二叉搜索树中搜索非常高效
缺点
- 时间消耗高,O(logn),在修正或平衡树,或者从指定位置获取元素
- 子节点不包含它父节点的信息,难于逆向遍历
- 只适用于排好序的列表,未排序的数据会降低到线性搜索
应用场景
- 非常适合于存储层级结构的数据,比如文件路径
- 用于实现顶级搜索和排序算法,比如二叉搜索树、二叉堆
Graph 图
图是一种数据结构,用于表示数据顶点(图的节点 vertex)之间可见的关系,而把顶点连接在一起的叫边(edge)。
边定义了哪些顶点连接在一起但没有在他们之间指定一个方向。每个顶点都有一些跟其他顶点的连接,这些信息以逗号分隔的list 形式保存在顶点中。
有一些特殊的图称为有向图,它定义了关系的方向,类似于链表。有向图在建立单向关系模型或流程图时非常有用。
它们主要用于用代码形式来传递可视化的web结构网络,这些结构可以模拟不同类型的关系,比如层级结构、分支结构,或者只是一个简单的无序的关系网。图的多功能性和直观性使其成为数据科学的最爱。
书写时,图有分别有一个顶点和边的列表:
V = {a, b, c, d, e}
E = {ab, ac, bd, cd, de}
Python中,图可以通过一个字典实现,每个顶点作为key,边的列表作为 value。
# Create the dictionary with graph elements
graph = { "a" : ["b","c"],
"b" : ["a", "d"],
"c" : ["a", "d"],
"d" : ["e"],
"e" : ["d"]
}
# Print the graph
print(graph)
优点
- 快速通过代码传递可视化的信息
- 可用于建模广泛的现实世界问题
- 语法简单
缺点
- 大图中,顶点的连接更难理解
- 从图中解析数据的时间消耗高
应用场景
- 优于模拟网络或 web类似的结构
- 适合模拟社交网络
哈希表 Hash table
哈希表是一种复杂的数据结构,适合存储大量数据,高效获取指定元素。
这种结构使用 key/value 对,键是元素的名称而值则是存储在此名称下的数据。
每个输入键都经过一个散列函数,它将其从起始形式转换为一个整数,称为散列。散列函数必须同一输入时总是生成相同的值,必须快速计算,并且长度相同。Python引入了一个内置的hash()
函数,用于提升速度。
哈希表使用散列来查找所需值的一般位置,称为存储桶。程序查找值时只需要搜索这个子组,而不是整个数据池。
超出这个一般框架之外,哈希表还可以根据应用程序的不同而有很大的不同。一些可能允许键值是不同的数据类型,另一些可能有不同设置的桶或不同的散列函数。
import pprint
class Hashtable:
def __init__(self, elements):
self.bucket_size = len(elements)
self.buckets = [[] for i in range(self.bucket_size)]
self._assign_buckets(elements)
def _assign_buckets(self, elements):
for key, value in elements: #calculates the hash of each key
hashed_value = hash(key)
index = hashed_value % self.bucket_size # positions the element in the bucket using hash
self.buckets[index].append((key, value)) #adds a tuple in the bucket
def get_value(self, input_key):
hashed_value = hash(input_key)
index = hashed_value % self.bucket_size
bucket = self.buckets[index]
for key, value in bucket:
if key == input_key:
return(value)
return None
def __str__(self):
return pprint.pformat(self.buckets) # pformat returns a printable representation of the object
if __name__ == "__main__":
capitals = [
('France', 'Paris'),
('United States', 'Washington D.C.'),
('Italy', 'Rome'),
('Canada', 'Ottawa')
]
hashtable = Hashtable(capitals)
print(hashtable)
print(f"The capital of Italy is {hashtable.get_value('Italy')}")
优点
- 转换任何格式的key到整型索引
- 对大的数据集非常高效
- 非常有效的搜索功能
- 每次搜索的步骤数保持不变,添加或删除元素的效率保持不变
- Python3做了些优化
缺点
- 散列值必须唯一,两个不同键转换成同一个值时会冲突
- 冲突错误需要彻底检查哈希函数
- 对新手来说太难
应用场景
- 适用于大的、经常搜索的数据库
- 使用输入键的检索系统