-
数据结构是指相互之间存在着一种或多种关系的数据元素的结合和该集合中数据元素之间的关系组成。
-
简单来说,数据结构就是设计数据 以何种方式组织并存储在计算机中
-
例如:列表、集合与字典等都是一种数据结构。
-
N.Wirth:"程序=数据结构+算法"
数据结构的分类
数据机构按照其逻辑结构可分为线性结构、树结构、图结构
- 线性结构:数据结构中的元素存在一对一的相互关系
- 树结构:数据结构中的元素存在一对多的相互关系
- 图结构:数据结构中的元素存在多对多的相互关系
1、列表/数组
列表(其它语言是数组)是一种基本数据类型。
关于列表问题:
1、列表的元素是如何存储的?(存内存地址(64位机器固定8bytes))
2、如何查找元素?(从列表起始位置的内存地址,查第几个元素就起始位置加n*8)
3、列表不用显示声明长度?(python内部通过完整拷贝来调整列表长度)
2、栈
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
栈的特点:后进先出 LIFO
栈的概念:栈顶,栈底
栈的基本操作:
- 进栈(压栈):push
- 出栈:pop
- 取栈顶:gettop
栈的实现
使用一般的列表即可实现栈
- 进栈:li.append
- 出栈:li.pop
- 取栈顶:li[-1]
class Stack: def __init__(self): self.stack = [] def push(self, data): self.stack.append(data) def pop(self): return self.stack.pop() def get_top(self): if len(self.stack) > 0: return self.stack[-1] else: return None def is_empty(self): return len(self.stack) > 0
栈的应用 -- 括号匹配问题
括号匹配问题:给一个字符串,其中包括小括号、中括号、大括号,求该字符串中的括号是否匹配。
例如:
-
- ()()[]{} 匹配
- ([{()}]) 匹配
- []( 不匹配
- ([)] 不匹配
def brace_match(s): match = {'}': '{', ']': '[', ')': '('} stack = Stack() for ch in s: if ch in {'(', '[', '{'}: stack.push(ch) else: # ch in {'}',']',')'} if stack.is_empty(): return False elif stack.get_top() == match[ch]: stack.pop() else: return False if stack.is_empty(): return True else: return False print(brace_match('[{()}(){()}[]({}){}]')) print(brace_match('[]}'))
3、队列
队列(queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
- 进行插入的一点称为队尾(rear),插入动作成为进队或入队。
- 进行删除的的一端称为对头(front),删除动作称为出队。
- 队列的性质:先进先出(First-in, First-out)
队列的实现
初步设想:列表+两个下标指针
创建一个列表和两个变量,front变量指向队首,rear变量指向队尾。初始时,front和rear都为0
进队操作:元素写到li[rear]的位置,rear自增1
出队操作:返回li[front]的元素,front自增1
出现d的情况后,队列无法继续添加元素,但是列表里面有位置是空的,这一点非常不好
队列的实现原理 -- 环形队列
环形队列:当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0
实现方式:求余运算
队首指针前进1:front = (front+1) % maxsize
队尾指针前进1:rear = (rear+1) % maxsize
队空条件:rear == front队满条件:(rear+1)% maxsize == front
class Queue: def __init__(self, size=100): self.queue = [0 for i in range(size)] self.size = size self.rear = 0 # 队尾指针 self.front = 0 # 队首指针 def push(self, element): if not self.is_filled(): self.rear = (self.rear + 1) % self.size self.queue[self.rear] = element else: raise IndexError("Queue is filled.") def pop(self): if not self.is_empty(): self.front = (self.front + 1) % self.size return self.queue[self.front] else: raise IndexError("Queue is empty.") # 判断队空 def is_empty(self): return self.rear == self.front # 判断队满 def is_filled(self): return (self.rear + 1) % self.size == self.front q = Queue(5) for i in range(4): q.push(i) print(q.pop()) q.push(4)
队列的内置模块
使用方法:from collections import deque
创建队列:queue = deque(li)
进队:append
出队:popleft
双向队列队首进队:appendleft
双向队列队尾进队:pop
栈和队列的应用 -- 迷宫问题
解决方法1:栈--深度优先搜索(回溯法)
思路:从一个节点开始,任意找下一个能走的点,当找不到能走的点时,退回上一个点寻找是否有其他方向的点。
(使用栈存储当前路径)
maze = [ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 1, 0, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 1, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 0, 0, 0, 1], [1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 1, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1, 1, 0, 1], [1, 1, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ] dirs = [ lambda x, y: (x+1, y), lambda x, y: (x, y+1), lambda x, y: (x-1, y), lambda x, y: (x, y-1) ] def maze_path(x1, y1, x2, y2): stack = [] stack.append((x1, y1)) while len(stack) > 0: curNode = stack[-1] # 当前节点 if curNode[0] == x2 and curNode[1] == y2: # 走到终点了 for p in stack: print(p) return True # x,y 四个方向 x+1, y; x, y+1; x-1, y; x, y-1 for dir in dirs: next_Node = dir(curNode[0], curNode[1]) # 如果下一个节点能走 if maze[next_Node[0]][next_Node[1]] == 0: stack.append(next_Node) maze[next_Node[0]][next_Node[1]] = 2 break else: # 一个都找不到 maze[next_Node[0]][next_Node[1]] = 2 stack.pop() else: print('没有路') return False maze_path(1,1,8,8)
4、链表
链表是由一系列节点组成的元素集合。每个元素包含两部分,数据域item和指向下一个节点的指针next。通过节点之间的相互连接,最终串联称一个链表。
创建链表
class Node: def __init__(self, data): self.data = data self.next = None # 头插法 def create_linklist(li): head = Node(li[0]) for num in li[1:]: node = Node(num) node.next = head head = node return head # 尾插法 def create_taillist(li): head = Node(li[0]) tail = head for num in li: node = Node(num) tail.next = node tail = node return head # 链表的遍历 def print_link(lk): while lk: print(lk.data, end=',') lk = lk.next
链表节点的插入和删除
插入:
p.next = curNode.next
curNode.next = p
删除:
curNode.next=p.next
del p
双链表
双链表的每个节点有两个指针:一个指向后一个节点,另一个指向前一个节点(单链表只有next)
双链表的插入和删除
插入:
p.next = curNode.next
curNode.next.prior = p
p.prior = curNode
curNode.next = p
删除:
curNode.next = p.next
p.next.prior = curNode
del p
链表与顺序表
- 链表在插入和删除的操作上明显快于顺序表
- 链表的内存可以更灵活的分配
5、哈希表
哈希表是一个通过哈希函数来计算数据存储位置的数据结构,通常支持如下操作:
- insert(key, value):插入键值对(key,value)
- get(key):如果存在键值对则返回其value,否则返回空值
- delete(key):删除键为key的键值对
知识储备-- 直接寻址表
当关键字的全域U比较小时,直接寻址表是一种简单而有效的方法
直接寻址技术缺点:
- 当域U很大时,需要消耗大量内存,很不实际
- 如果域很大而实际出现的key很小,则大量空间被浪费
- 无法处理关键字不是数字的情况
改进直接寻址表:哈希
- 构建大小为m的寻址表T
- key为k的元素放到h(k)的位置上
- h(k)是一个函数,其将域U映射到表T[0, 1, ..., m-1]
哈希表(又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下标。
假设有一个长度为7的哈希表,哈希函数h(k)=k%7 元素集合{14, 22, 3, 5}的存储方式如下图:
哈希冲突
由于哈希表的大小是有限的,而要存储的值的总数量是无限的,因为对于任何哈希函数,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突!
比如h(k)=k%7, h(0)=h(7)=h(14)...
解决哈希冲突的两个办法:
1、开放寻执法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
- 线性探查:如果位置i被占用,则探查i+1,i+2,...
- 二次探查:如果位置i被占用,则探查则探查i+12,i-12,i+22,i-22,……(2表示平方)
- 二度哈希:有n个哈希函数,当使用第一个哈希函数h1发生冲突时,则尝试使用h2,h3,...
2、拉链法:哈希表每个位置都连接一个链表,当冲突发生时,冲突的元素将被加到该位置链表的最后
拉链法解决hash冲突代码实现:
# 链表类 class LinkList: class Node: def __init__(self, item=None): self.item = item self.next = None class LinkListIterator: def __init__(self, node): self.node = node def __next__(self): if self.node: cur_node = self.node self.node = cur_node.next return cur_node.item else: raise StopIteration def __iter__(self): return self def __init__(self, iterable=None): self.head = None self.tail = None if iterable: self.extend(iterable) def append(self, obj): s = LinkList.Node(obj) if not self.head: self.head = s self.tail = s else: self.tail.next = s self.tail = s def extend(self, iterable): for obj in iterable: self.append(obj) def find(self, obj): for n in self: if n == obj: return True else: return False def __iter__(self): return self.LinkListIterator(self.head) def __repr__(self): return "<<"+", ".join(map(str, self))+">>" # 类似于集合的结构 class HashTable: def __init__(self, size=101): self.size = size self.T = [LinkList() for i in range(self.size)] def h(self, k): return k % self.size def insert(self, k): i = self.h(k) if self.find(k): print("Duplicated Insert") else: self.T[i].append(k) def find(self, k): i = self.h(k) return self.T[i].find(k) ht = HashTable() ht.insert(0) ht.insert(1) ht.insert(3) ht.insert(102) ht.insert(508) print(",".join(map(str, ht.T))) print(ht.find(203))
哈希表 -- 常见哈希函数
除法哈希法: h(k) = k % m
乘法哈希法: h(k) = floor(m*(A*Key%1))
全域哈希法: ha,b(k) = ((a*key + b) mod p) mod m a,b=1,2,...,p-1
哈希表的应用 -- 集合与字典
字典与集合都是通过哈希表来实现的。
a = {'name':xcq, 'age':18, 'gender':'Man'}
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。假设h('name')=3, h('age'=1),h('gender'=4), 则哈希表存储为[Node, 18, Node, 'xcq', 'Man']
如果发生哈希冲突,则通过拉链法或则开放寻址法解决。
6、二叉树
树是一种数据结构 比如:目录结构
树是一种可以递归定义的数据结构 树是由n个节点组成的集合
如果n=0, 那这是一颗空树
如果n>0, 那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树
二叉树:度不超过2的树(节点最多有两个叉)
二叉树的链式存储:
将二叉树的节点定义为一个对象,节点之间通过类似链表的连接方式来连接
二叉树的4种遍历方式:
- 前序遍历:EACBDGF
- 中序遍历:ABCDEGF
- 后序遍历:BDCAFGE
- 层次遍历:EAGCFBD
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: post_order(root.lchild) post_order(root.rchild) print(root.data, end=',') # 层级遍历 from collections import deque def level_order(root): que = deque() que.append(root) while len(que): node = que.popleft() print(node.data, end=',') if node.lchild: que.append(node.lchild) if node.rchild: que.append(node.rchild)
二叉搜索树
二叉搜索树是一棵二叉树且满足性质:设x是二叉树的一个节点。如果y是x的左子树的一个节点,那么y.key <= x.key;如果y是x的右子树的一个节点,那么y.key >= x.key
(左边的都比根节点小,右边的都比根节点大)
二叉搜索树的操作:查询、插入、删除
需要注意的是删除操作
删除操作分3种情况:
1、如果要删除的节点是叶子节点:直接删除
2、如果要删除的节点只有一个孩子:将此节点的父亲与孩子连接,然后删除该节点
3、如果要删除的节点有两个孩子:将其右子树最小节点删除,并替换当前节点。
二叉搜索树查询、插入、删除代码实现
import random class BiTreeNode: def __init__(self, data): self.data = data self.lchild = None # 左孩子 self.rchild = None # 右孩子 self.parent = None class BST: def __init__(self, li=None): self.root = None if li: for val in li: self.insert_no_rec(val) # 插入(递归) def insert(self, node, val): if not node: node = BiTreeNode(val) elif val < node.data: node.lchild = self.insert(node.lchild, val) node.lchild.parent = node elif val > node.data: node.rchild = self.insert(node.rchild, val) node.rchild.parent = node return node # 插入(非递归) def insert_no_rec(self, val): p = self.root if not p: # 空树 self.root = BiTreeNode(val) return while True: if val < p.data: if p.lchild: p = p.lchild else: # 左孩子不存在 p.lchild = BiTreeNode(val) p.lchild.parent = p return elif val > p.data: if p.rchild: p = p.rchild else: p.rchild = BiTreeNode(val) p.rchild.parent = p return else: return # 查询(递归) def query(self, node, val): if not node: return None if node.data < val: return self.query(node.rchild, val) elif node.data > val: return self.query(node.lchild, val) else: return node # 查询(非递归) def query_no_rec(self, val): p = self.root while p: if p.data < val: p = p.rchild elif p.data > val: p = p.lchild else: return p return None def pre_order(self, root): if root: print(root.data, end=',') self.pre_order(root.lchild) self.pre_order(root.rchild) def in_order(self, root): if root: self.in_order(root.lchild) print(root.data, end=',') self.in_order(root.rchild) def post_order(self, root): if root: self.post_order(root.lchild) self.post_order(root.rchild) print(root.data, end=',') def __remove_node_1(self, node): # 情况1:node是叶子节点 if not node.parent: self.root = None if node == node.parent.lchild: #node是它父亲的左孩子 node.parent.lchild = None else: #右孩子 node.parent.rchild = None def __remove_node_21(self, node): # 情况2.1:node只有一个左孩子 if not node.parent: # 根节点 self.root = node.lchild node.lchild.parent = None elif node == node.parent.lchild: node.parent.lchild = node.lchild node.lchild.parent = node.parent else: node.parent.rchild = node.lchild node.lchild.parent = node.parent def __remove_node_22(self, node): # 情况2.2:node只有一个右孩子 if not node.parent: self.root = node.rchild elif node == node.parent.lchild: node.parent.lchild = node.rchild node.rchild.parent = node.parent else: node.parent.rchild = node.rchild node.rchild.parent = node.parent def delete(self, val): if self.root: # 不是空树 node = self.query_no_rec(val) if not node: # 不存在 return False if not node.lchild and not node.rchild: #1. 叶子节点 self.__remove_node_1(node) elif not node.rchild: # 2.1 只有一个左孩子 self.__remove_node_21(node) elif not node.lchild: # 2.2 只有一个右孩子 self.__remove_node_22(node) else: # 3. 两个孩子都有 min_node = node.rchild while min_node.lchild: min_node = min_node.lchild node.data = min_node.data # 删除min_node if min_node.rchild: self.__remove_node_22(min_node) else: self.__remove_node_1(min_node) # # # tree = BST([1,4,2,5,3,8,6,9,7]) # tree.in_order(tree.root) # print("") # # tree.delete(4) # tree.delete(1) # tree.delete(8) # tree.in_order(tree.root)
二叉搜索树的效率
平均情况下,二叉搜索树进行搜索的时间复杂度为O(nlogn)
最坏情况下,二叉搜索树可能非常偏斜(线性)
解决方案:
- 随机化插入
- AVL树
7、AVL树
AVL树是一颗自平衡的二叉搜索树。
AVL树具有一下性质:
- 根的左右子树的高度差的绝对值不能超过1
- 根的左右子树都是平衡二叉树
AVL树--插入
插入一个节点可能会破坏AVL树的平衡,可以通过旋转操作来进行修正。
插入一个节点后,只有从插入节点到根节点的路径上的节点的平衡可能被改变。我们需要找出第一个破坏了平衡条件的节点,称之为K。k的左右子树高度差的绝对值为2
不平衡的出现可能会有4种情况
1、左旋
不平衡是由于对k的右孩子的右子树插入导致的
2、右旋
不平衡是由于对k的左孩子的左子树插入导致的
3、右旋-左旋
不平衡是由于对K的右孩子的左子树插入导致的
4、左旋-右旋
AVL树插入操作代码实现:
from bst import BiTreeNode, BST class AVLNode(BiTreeNode): def __init__(self, data): BiTreeNode.__init__(self, data) self.bf = 0 class AVLTree(BST): def __init__(self, li=None): BST.__init__(self, li) def rotate_left(self, p, c): s2 = c.lchild p.rchild = s2 if s2: s2.parent = p c.lchild = p p.parent = c p.bf = 0 c.bf = 0 return c def rotate_right(self, p, c): s2 = c.rchild p.lchild = s2 if s2: s2.parent = p c.rchild = p p.parent = c p.bf = 0 c.bf = 0 return c def rotate_right_left(self, p, c): g = c.lchild s3 = g.rchild c.lchild = s3 if s3: s3.parent = c g.rchild = c c.parent = g s2 = g.lchild p.rchild = s2 if s2: s2.parent = p g.lchild = p p.parent = g # 更新bf if g.bf > 0: p.bf = -1 c.bf = 0 elif g.bf < 0: p.bf = 0 c.bf = 1 else: # 插入的是g p.bf = 0 c.bf = 0 return g def rotate_left_right(self, p, c): g = c.rchild s2 = g.lchild c.rchild = s2 if s2: s2.parent = c g.lchild = c c.parent = g s3 = g.rchild p.lchild = s3 if s3: s3.parent = p g.rchild = p p.parent = g # 更新bf if g.bf < 0: p.bf = 1 c.bf = 0 elif g.bf > 0: p.bf = 0 c.bf = -1 else: p.bf = 0 c.bf = 0 return g def insert_no_rec(self, val): # 1、和BST一样插入(AVL要一边插入一边控制balance factor) p = self.root if not p: # 空树 self.root = AVLNode(val) return while True: if val < p.data: if p.lchild: p = p.lchild else: # 左孩子不存在 p.lchild = AVLNode(val) p.lchild.parent = p node = p.lchild # node 存储的就是插入的节点 break elif val > p.data: if p.rchild: p = p.rchild else: p.rchild = AVLNode(val) p.rchild.parent = p node = p.rchild break else: # val == p.data return # 2、更新balance factor while node.parent: # node.parent 不空 if node.parent.lchild == node: # 传递是从左子树来的, 左子树更沉了 # 更新node.parent的bf -= 1 if node.parent.bf < 0: # 原来node.parent.bf = -1, 更新后变成-2 # 做旋转 # 看node那边沉 g = node.parent.parent # 为了链接旋转后的子树 x = node.parent if node.bf > 0: n = self.rotate_left_right(node.parent, node) else: n = self.rotate_right(node.parent, node) # 记得:把n和g连起来 elif node.parent.bf > 0: # 原来node.parent.bf = 1更新后等于0 node.parent.bf = 0 break else: # 原来node.parent.bf = 0, 更新之后变成-1 node.parent.bf = -1 node = node.parent continue else: # 传递是从右子树来的,右子树更沉了 # 更新node.parent.bf += 1 if node.parent.bf > 0: # 原来node.parent.bf =1< 更新后变成2 # 做旋转 # 看node哪边沉 g = node.parent.parent # 为了连接旋转之后的子树 x = node.parent # 旋转前的子树的根 if node.bf < 0: # node.bf = -1 n = self.rotate_right_left(node.parent, node) else: n = self.rotate_left(node.parent, node) # 记得连起来 elif node.parent.bf < 0: # 原来node.parent.bf = -1, 更新之后变成0 node.parent.bf = 0 break else: # 原来node.parent.bf = 0, 更新之后变成1 node.parent.bf = 1 node = node.parent continue # 连接旋转后的子树: n.parent = g if g: # g不是空 if x == g.lchild: g.lchild = n else: g.rchild = n break else: self.root = n break