数据结构的存储方式
数据结构的底层存储方式只有数组(顺序存储)和链表(链式存储)
- 数组是紧凑连续存储,相对节约空间,可以索引快速访问,但如果连续内存空间不够则需要将数据复制到一块更大的空间中,扩容复杂度O(N),插入删除也需要移动大量元素,复杂度O(N)
- 链表的元素不连续,不存在扩容问题,插入删除复杂度O(1),但不能随机访问(任意访问某个下标处的元素),且存储前后元素的指针要消耗额外空间
数组和链表是一切的底层基础,其他任何数据都是上层建筑
- 图的两种表示:邻接矩阵就是二维数组,但在稀疏图中耗费空间;邻接表就是链表,节省空间但很多操作效率不如邻接矩阵
- 树一般用链表实现,还有各种巧妙的设计(如二叉搜索树、AVL树、红黑树、区间树、B树);特别的,如果是完全二叉树,则可用数组存储树,无需保存额外的结点指针、还能简化操作,也就是“堆”
数据结构的基本操作
任何数据结构的操作,概括而言都是遍历+访问
我们选择不同种类的数据结构,目的就是在不同场景下高效地增、删、查、改
遍历+访问的两种形式:线性和非线性
线性:数组 的for/while迭代遍历、链表 的for/while迭代遍历
def traverse(arr: list):
"""遍历数组"""
for i in range(len(arr)):
# 访问arr[i]
class ListNode(object):
"""单链表的结点"""
def __init__(self, data):
self.data = data
self.next = None
def traverse(head: ListNode):
"""遍历单链表"""
p = head
while True:
# 访问p结点
p = p.next
if head.next is None:
break
非线性:链表 的递归遍历
class ListNode(object):
"""单链表的结点"""
def __init__(self, data):
self.data = data
self.next = None
def traverse(head: ListNode):
"""递归遍历单链表"""
# 在此前序遍历head.data
traverse(head.next)
# 在此后序遍历head.data
链表的情况稍作推广,得到二叉树的递归遍历结构
class TreeNode(object):
"""二叉树的结点"""
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def traverse(root: TreeNode):
"""二叉树的递归遍历"""
# 在此前序遍历
traverse(root.left)
# 在此中序遍历
traverse(root.right)
# 在此后序遍历
在推广至更一般情况,得到N叉树的递归遍历结构
class TreeNode(object):
"""N叉树的结点"""
def __init__(self, val):
self.val = val
self.children = []
def traverse(root: TreeNode):
"""N叉树的递归遍历"""
for child in root.children:
traverse(child)
框架思维
上面建立起了核心框架,无论任何数据结构的增删查改,都脱离不开上面的框架结构,只需根据具体问题添加代码就好了
遇到问题时,把握其共性和本质,往框架上靠,从而举一反三
算法学习路线
数据结构是工具,算法就是选用合适工具来解决问题,因此先掌握常用数据结构
先从二叉树入手,由二叉树培养框架思维,且大多算法本质上都是树的遍历问题,上文提到的框架几乎能解决所有二叉树问题
def traverse(root: TreeNode):
"""二叉树的递归遍历"""
# 在此前序遍历
traverse(root.left)
# 在此中序遍历
traverse(root.right)
# 在此后序遍历
刷完二叉树,再去做回溯、分治、动态规划,会发现只要涉及递归的问题,基本上都是树的问题——这些问题可以用递归树来画图表示,树上的某一个节点,就是一个递归函数的返回值
动态规划, 分治算法, 回溯(DFS)算法, BFS 算法,就是在“递归树”这棵抽象的“树”中搜索目标的过程
并查集算法, 二叉堆与优先级队列,则是利用 数组 来存储 树,并实现一些特殊功能
- 动态规划的凑零钱问题,暴力就是遍历一颗N叉树:每次从当前结点做选择,并进入下一节点
def dp(n):
for coin in coins:
dp(n-coin)
- 回溯算法就是一个N叉树的前序+后序遍历问题,比如排列组合问题
def backtrack(nums: list, ans: list):
"""打印nums的所有排列组合"""
if len(ans) == len(nums):
print(ans)
for num in nums:
if num not in ans:
ans.append(num) # 在前序遍历位置做出选择
backtrack(nums, ans) # 进入下一决策树
ans.pop() # 在后序遍历位置撤销选择
backtrack([1,2,3],[])