1、数据结构简介
数据结构是为实现对计算机数据有效使用的各种数据组织形式,服务于各类计算机操作。不同的数据结构具有各自对应的适用场景,旨在降低各种算法计算的时间与空间复杂度,达到最佳的任务执行效率。
如下图所示,常见的数据结构可分为「线性数据结构」与「非线性数据结构」,具体为:「数组」、「链表」、「栈」、「队列」、「树」、「图」、「散列表」、「堆」。
2、数组:Array
数组是将相同类型的元素存储于连续内存空间的数据结构,其长度不可变。
如下图所示,构建此数组需要在初始化时给定长度,并对数组每个索引元素赋值,代码如下:
array = [2, 3, 1, 0, 2];
「可变数组」是经常使用的数据结构,其基于数组和扩容机制实现,相比普通数组更加灵活。常用操作有:访问元素、添加元素、删除元素。
# 初始化可变数组
array = []
# 向尾部添加元素
array.append(2)
array.append(3)
array.append(1)
array.append(0)
array.append(2)
2.1 数组练习题1
给定一个二进制数组, 计算其中最大连续 1 的个数。
示例:
输入:[1,1,0,1,1,1]
输出:3
解释:开头的两位和最后的三位都是连续 1 ,所以最大连续 1 的个数是 3.
提示:
输入的数组只包含 0 和 1 。
输入数组的长度是正整数,且不超过 10,000。
class Solution:
def findMaxConsecutiveOnes(self, nums: List[int]) -> int:
count,result=0,0 # 计数器
for i in range(len(nums)):
if nums[i]==1: # 遍历列表,当值为1,count的值加1
count+=1
else:
result=max(count,result) # 当值不为1,获取最大的值
count=0 # 将计数器赋值为0,从新计数
return max(result,count)
2.2 数组练习题2
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
index=0
for i in range(len(nums)):
if nums[i] !=0: # 当num[i]不为0
nums[index]=nums[i] # num[index]赋值为num[i]
index +=1
for j in range(len(nums)): #遍历列表
if j>=index: # 当j的值大于index的值,num[j]赋值为0
nums[j]=0
return nums
3、链表:Linked List
链表以节点为单位,每个元素都是一个独立对象,在内存空间的存储是非连续的。链表的节点对象具有两个成员变量:「值 val」,「后继节点引用 next」 。
class ListNode:
def __init__(self, val, next=None):
self.val = val # 节点值
self.next = next # 引用后继节点
如下图所示,建立此链表需要实例化每个节点,并构建各节点的引用指向。
# 实例化节点
n1 = ListNode(4) # 节点 head
n2 = ListNode(5)
n3 = ListNode(1)
# 构建引用指向
n1.next = n2
n2.next = n3
3.1 链表练习题1:合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def mergeTwoLists(self, L1: ListNode, L2: ListNode) -> ListNode:
limt=ListNode(-1)
prev=limt
while L1 and L2:
if L1.val<=L2.val:
prev.next,L1=L1,L1.next
else:
prev.next,L2=L2,L2.next
prev=prev.next
prev.next=L1 if L1 is not None else L2
return limt.next
3.2 链表练习题2:反转一个单链表
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
进阶:
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
n1,n2=head,None
while n1:
temp=n1.next # temp临时指针 赋值给head下个值
n1.next=n2 # n1.next=n2
n2=n1 # n2=n1
n1=temp # n1=temp temp: [2,3]
return n2
3.3 链表练习题3:双指针
题目:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
示例1:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, L1: ListNode, L2: ListNode) -> ListNode:
cur=tep=ListNode(0)
while L1 and L2:
if L1.val<L2.val:
cur.next,L1=L1,L1.next
else:
cur.next,L2=L2,L2.next
cur=cur.next
cur.next = L1 if L1 else L2
# Python 三元表达式写法 A if x else B ,代表当 x = True 时执行 A ,否则执行 B
return tep.next
4、栈:Stack
栈是一种具有 「先入后出」 特点的抽象数据结构,可使用数组或链表实现。
stack = [] # Python 可将列表作为栈使用
如下图所示,通过常用操作「入栈 push()」,「出栈 pop()」,展示了栈的先入后出特性。
stack.append(1) # 元素 1 入栈
stack.append(2) # 元素 2 入栈
stack.pop() # 出栈 -> 元素 2
stack.pop() # 出栈 -> 元素 1
5、队列:Queue
队列是一种具有 「先入先出」 特点的抽象数据结构,可使用链表实现。
# Python 通常使用双端队列 collections.deque
from collections import deque
queue = deque()
如下图所示,通过常用操作「入队 push()」,「出队 pop()」,展示了队列的先入先出特性。
queue.append(1) # 元素 1 入队
queue.append(2) # 元素 2 入队
queue.popleft() # 出队 -> 元素 1
queue.popleft() # 出队 -> 元素 2
6、树:Tree
树是一种非线性数据结构,根据子节点数量可分为 「二叉树」 和 「多叉树」,最顶层的节点称为「根节点 root」。以二叉树为例,每个节点包含三个成员变量:「值 val」、「左子节点 left」、「右子节点 right」 。
class TreeNode:
def __init__(self, x):
self.val = x # 节点值
self.left = None # 左子节点
self.right = None # 右子节点
如下图所示,建立此二叉树需要实例化每个节点,并构建各节点的引用指向。
# 初始化节点
n1 = TreeNode(3) # 根节点 root
n2 = TreeNode(4)
n3 = TreeNode(5)
n4 = TreeNode(1)
n5 = TreeNode(2)
# 构建引用指向
n1.left = n2
n1.right = n3
n2.left = n4
n2.right = n5
6.1 前序遍历
二叉树的前序遍历:首先访问根节点,然后遍历左子树,最后遍历右子树。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def preordef(root):
if not root: return
res.append(root.val)
preordef(root.left)
preordef(root.right)
preordef(root)
return res
6.2 中序遍历
中序遍历:先遍历左子树,然后访问根节点,然后遍历右子树。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def preordef(root):
if not root: return
preordef(root.left)
res.append(root.val)
preordef(root.right)
preordef(root)
return res
6.3 后序遍历
后序遍历:先遍历左子树,然后遍历右子树,最后访问树的根节点。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def preordef(root):
if not root: return
preordef(root.left)
preordef(root.right)
res.append(root.val)
preordef(root)
return res
6.4 层序遍历
该算法从一个根节点开始,首先访问节点本身。 然后遍历它的相邻节点,其次遍历它的二级邻节点、三级邻节点,以此类推。 当我们在树中进行广度优先搜索时,我们访问的节点的顺序是按照层序遍历顺序的。
7、图:Graph
图是一种非线性数据结构,由「节点(顶点)vertex」和「边 edge」组成,每条边连接一对顶点。根据边的方向有无,图可分为「有向图」和「无向图」。本文 以无向图为例 开展介绍。
如下图所示,此无向图的 顶点 和 边 集合分别为:
顶点集合: vertices = {1, 2, 3, 4, 5} 边集合: edges = {(1, 2), (1, 3), (1, 4), (1, 5), (2, 4), (3, 5), (4, 5)}
表示图的方法通常有两种:
- 邻接矩阵: 使用数组 vertices 存储顶点,邻接矩阵 edges 存储边; edges[i][j] 代表节点 i + 1和 节点 j + 1 之间是否有边。
vertices = [1, 2, 3, 4, 5]
edges = [[0, 1, 1, 1, 1],
[1, 0, 0, 1, 0],
[1, 0, 0, 0, 1],
[1, 1, 0, 0, 1],
[1, 0, 1, 1, 0]]
- 邻接表: 使用数组 vertices 存储顶点,邻接表 edges 存储边。 edges 为一个二维容器,第一维 i 代表顶点索引,第二维 edges[i] 存储此顶点对应的边集和;例如 edges[0] = [1, 2, 3, 4]代表 vertices[0] 的边集合为 [1, 2, 3, 4] 。
vertices = [1, 2, 3, 4, 5]
edges = [[1, 2, 3, 4],
[0, 3],
[0, 4],
[0, 1, 4],
[0, 2, 3]]
邻接矩阵 VS 邻接表 :
邻接矩阵的大小只与节点数量有关,即 N^2,其中 N 为节点数量。因此,当边数量明显少于节点数量时,使用邻接矩阵存储图会造成较大的内存浪费。 因此,邻接表 适合存储稀疏图(顶点较多、边较少); 邻接矩阵 适合存储稠密图(顶点较少、边较多)。
8、散列表:Hash表
散列表是一种非线性数据结构,通过利用 Hash 函数将指定的「键 key」映射至对应的「值 value」,以实现高效的元素查找。
设想一个简单场景:小力、小特、小扣的学号分别为 10001, 10002, 10003 。
现需求从「姓名」查找「学号」。
则可通过建立姓名为 key ,学号为 value 的散列表实现此需求,代码如下:
# 初始化hash表
dic = {}
# 添加 key -> value 键值对
dic["小力"] = 10001
dic["小特"] = 10002
dic["小扣"] = 10003
# 从姓名查找学号
dic["小力"] # -> 10001
dic["小特"] # -> 10002
dic["小扣"] # -> 10003
自行设计 Hash 函数:
假设需求:从「学号」查找「姓名」。
将三人的姓名存储至以下数组中,则各姓名在数组中的索引分别为 0, 1, 2 。
names = [ "小力", "小特", "小扣" ]
此时,我们构造一个简单的 Hash 函数( % 为取余符号 ),公式和封装函数如下所示:
hash(key)=(key−1)%10000
def hash(id):
index = (id - 1) % 10000
return index
则我们构建了以学号为 key 、姓名对应的数组索引为 value 的散列表。利用此 Hash 函数,则可在 O(1) 时间复杂度下通过学号查找到对应姓名,即:
names[hash(10001)] // 小力
names[hash(10002)] // 小特
names[hash(10003)] // 小扣
以上设计只适用于此示例,实际的 Hash 函数需保证低碰撞率、 高鲁棒性等,以适用于各类数据和场景。
9、堆:Heap
堆是一种基于「完全二叉树」的数据结构,可使用数组实现。以堆为原理的排序算法称为「堆排序」,基于堆实现的数据结构为「优先队列」。堆分为「大顶堆」和「小顶堆」,大(小)顶堆:任意节点的值不大于(小于)其父节点的值。
完全二叉树定义: 设二叉树深度为 k ,若二叉树除第 k 层外的其它各层(第 11 至 k-1层)的节点达到
最大个数,且处于第 k 层的节点都连续集中在最左边,则称此二叉树为完全二叉树。
如下图所示,为包含 1, 4, 2, 6, 8 元素的小顶堆。将堆(完全二叉树)中的结点按层编号,即可映射到右边的数组存储形式。
通过使用「优先队列」的「压入 push()」和「弹出 pop()」操作,即可完成堆排序,实现代码如下:
from heapq import heappush, heappop
# 初始化小顶堆
heap = []
# 元素入堆
heappush(heap, 1)
heappush(heap, 4)
heappush(heap, 2)
heappush(heap, 6)
heappush(heap, 8)
# 元素出堆(从小到大)
heappop(heap) # -> 1
heappop(heap) # -> 2
heappop(heap) # -> 4
heappop(heap) # -> 6
heappop(heap) # -> 8
# 「最大堆」的实现
import sys
class MaxHeap:
def __init__(self, heapSize):
# heapSize用于数组的大小,因为数组在创建的时候至少需要指明数组的元素个数
self.heapSize = heapSize
# 使用数组创建完全二叉树的结构,然后使用二叉树构建一个「堆」
self.maxheap = [0]*(heapSize+1)
# realSize用于记录「堆」的元素个数
self.realSize = 0
# 添加元素函数
def add(self, element):
self.realSize += 1
# 如果「堆」中元素的个数大于一开始设定的数组的个数,则返回「Add too many elements」
if self.realSize > self.heapSize:
print("Add too many elements!")
self.realSize -= 1
return
# 将添加的元素添加到数组中
self.maxheap[self.realSize] = element
# 新增元素的索引位置
index = self.realSize
# 新增元素的父节点的索引位置
# 注意,如果用数组表示完全二叉树,并且根结点存储在数组的索引1的位置的时候,任何一个节点的父节点索引位置为「该节点的索引位置/2」,任何一个节点的左孩子节点的索引位置为「该节点的索引位置*2」,任何一个节点的右孩子节点的索引位置为「该节点的索引位置*2+1」
parent = index // 2
# 当添加的元素大于父节点时,需要将父节点的值和新增元素的值交换
while (self.maxheap[index] > self.maxheap[parent] and index > 1):
self.maxheap[parent], self.maxheap[index] = self.maxheap[index], self.maxheap[parent]
index = parent
parent = index // 2
# 获取堆顶元素函数
def peek(self):
return self.maxheap[1]
# 删除堆顶元素函数
def pop(self):
# 如果当前「堆」的元素个数为0, 则返回「Don't have any element」
if self.realSize < 1:
print("Don't have any element!")
return sys.maxsize
else:
# 当前「堆」中含有元素
# self.realSize >= 1
removeElement = self.maxheap[1]
# 将「堆」中的最后一个元素赋值给堆顶元素
self.maxheap[1] = self.maxheap[self.realSize]
self.realSize -= 1
index = 1
# 当删除的元素不是孩子节点时
while (index < self.realSize and index <= self.realSize // 2):
# 被删除节点的左孩子节点
left = index * 2
# 被删除节点的右孩子节点
right = (index * 2) + 1
# 当删除节点的元素小于 左孩子节点或者右孩子节点,代表该元素的值小,此时需要将该元素与左、右孩子节点中最大的值进行交换
if (self.maxheap[index] < self.maxheap[left] or self.maxheap[index] < self.maxheap[right]):
if self.maxheap[left] > self.maxheap[right]:
self.maxheap[left], self.maxheap[index] = self.maxheap[index], self.maxheap[left]
index = left
else:
self.maxheap[right], self.maxheap[index] = self.maxheap[index], self.maxheap[right]
index = right
else:
break
return removeElement
# 返回「堆」的元素个数
def size(self):
return self.realSize
def toString(self):
print(self.maxheap[1 : self.realSize+1])
if __name__ == "__main__":
# 测试用例
maxHeap = MaxHeap(5)
maxHeap.add(1)
maxHeap.add(2)
maxHeap.add(3)
# [3,1,2]
maxHeap.toString()
# 3
print(maxHeap.peek())
# 3
print(maxHeap.pop())
# 2
print(maxHeap.pop())
# 1
print(maxHeap.pop())
maxHeap.add(4)
maxHeap.add(5)
# [5,4]
maxHeap.toString()
# 「最小堆」的实现
import sys
class MinHeap:
def __init__(self, heapSize):
# heapSize用于数组的大小,因为数组在创建的时候至少需要指明数组的元素个数
self.heapSize = heapSize
# 使用数组创建完全二叉树的结构,然后使用二叉树构建一个「堆」
self.minheap = [0]*(heapSize+1)
# realSize用于记录「堆」的元素个数
self.realSize = 0
# 添加元素函数
def add(self, element):
self.realSize += 1
# 如果「堆」中元素的个数大于一开始设定的数组的个数,则返回「Add too many elements」
if self.realSize > self.heapSize:
print("Add too many elements!")
self.realSize -= 1
return
# 将添加的元素添加到数组中
self.minheap[self.realSize] = element
# 新增元素的索引位置
index = self.realSize
# 新增元素的父节点的索引位置
# 注意,如果用数组表示完全二叉树,并且根结点存储在数组的索引1的位置的时候,任何一个节点的父节点索引位置为「该节点的索引位置/2」,任何一个节点的左孩子节点的索引位置为「该节点的索引位置*2」,任何一个节点的右孩子节点的索引位置为「该节点的索引位置*2+1」
parent = index // 2
# 当添加的元素小于父节点时,需要将父节点的值和新增元素的值交换
while (self.minheap[index] < self.minheap[parent] and index > 1):
self.minheap[parent], self.minheap[index] = self.minheap[index], self.minheap[parent]
index = parent
parent = index // 2
# 获取堆顶元素函数
def peek(self):
return self.minheap[1]
# 删除堆顶元素函数
def pop(self):
# 如果当前「堆」的元素个数为0, 则返回「Don't have any element」
if self.realSize < 1:
print("Don't have any element!")
return sys.maxsize
else:
# 当前「堆」中含有元素
# self.realSize >= 1
removeElement = self.minheap[1]
# 将「堆」中的最后一个元素赋值给堆顶元素
self.minheap[1] = self.minheap[self.realSize]
self.realSize -= 1
index = 1
# 当删除的元素不是孩子节点时
while (index < self.realSize and index <= self.realSize // 2):
# 被删除节点的左孩子节点
left = index * 2
# 被删除节点的右孩子节点
right = (index * 2) + 1
# 当删除节点的元素大于 左孩子节点或者右孩子节点,代表该元素的值大,此时需要将该元素与左、右孩子节点中最小的值进行交换
if (self.minheap[index] > self.minheap[left] or self.minheap[index] > self.minheap[right]):
if self.minheap[left] < self.minheap[right]:
self.minheap[left], self.minheap[index] = self.minheap[index], self.minheap[left]
index = left
else:
self.minheap[right], self.minheap[index] = self.minheap[index], self.minheap[right]
index = right
else:
break
return removeElement
# 返回「堆」的元素个数
def size(self):
return self.realSize
def toString(self):
print(self.minheap[1 : self.realSize+1])
if __name__ == "__main__":
# 测试用例
minHeap = MinHeap(5)
minHeap.add(3)
minHeap.add(1)
minHeap.add(2)
# [1,3,2]
minHeap.toString()
# 1
print(minHeap.peek())
# 1
print(minHeap.pop())
# 2
print(minHeap.pop())
# 3
print(minHeap.pop())
minHeap.add(4)
minHeap.add(5)
# [4,5]
minHeap.toString()