一、数据结构的定义
官方定义:
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
简单理解:
数据结构就是设计数据以何种方式组织并存储在计算机中。比如:列表、集合与字典等都是一种数据结构。
程序=数据结构+算法
二、数据结构的分类
数据结构按照其逻辑结构可分为线性结构、树结构、图结构
线性结构:数据结构中的元素存在一对一的相互关系(列表)
树结构:数据结构中的元素存在一对多的相互关系(堆)
图结构:数据结构中的元素存在多对多的相互关系
-----python的列表
在其他编程语言中被称为“数组”,在数据结构中可以统称为“顺序表”,即在内存的存储中是线性的。
列表和数组的不同之处:
1.数组要求存储的数据类型是一样的。python的列表没有要求。
2.数组的长度在声明时就固定了,不可变的。python的列表长度可变。
python中的列表在数据结构学科中又被称作是动态表,它在内存中的存储方式大概是这样的,当你创建一个列表时,系统会先分配一块特定的内存,当列表中元素增满时,系统会再从新开辟一块之前容量2倍的新内存,然后将之前的数据copy到新开内存中,释放原来的旧地址,反之也是。通过这种动态扩张的形式让 python的列表保证了长度可变。
数组通过规定存储的数据类型是相同的,同时是线性存储的特点,通过数组的首地址和下标的关系保证了查找和存储数据的高效。而python的列表通过存储元素内存指针的方式既可以保证可以存储不同数据类型的数据,又可以保证查询和存储的高效(python在内存存储方面有一个小数据池的概念,它在python实现数据存储上起到了很大的作用)。
----栈
栈(Stack)是一个数据集合,可以理解为只能在一端进行插入或删除操作的列表。
栈的特点是:后进先出(last-in, first-out)栈有栈顶和栈底两个基础概念。python语言实现栈的方式:
借助python的列表及两个基本的操作append和pop方法就可以实现栈。
class Stark(list):
def __init__(self, *args, **kwargs):
super(Stark, self).__init__(*args, **kwargs)
def push(self, val):
self.append(val)
def _pop(self):
return self.pop()
栈的实际应用(括号匹配问题)
括号匹配问题:
给一个字符串,其中包含小括号、中括号、大括号,求该字符串中的括号是否匹配。
例如:
()()[]{} 匹配
([{()}]) 匹配
[]( 不匹配
[(]) 不匹配
def brace_match(s):
stack = []
match = {')':'(', ']':'[', '}':'{'}
match2 = {'(':')', '[':']', '{':'}'}
for ch in s:
if ch in {'(', '[', '{'}:
stack.append(ch)
elif len(stack) == 0:
print("缺少%s" % match[ch])
return False
elif stack[-1] == match[ch]:
stack.pop()
else:
print("括号不匹配")
return False
if len(stack) > 0:
print("缺少%s" % (match2[stack[-1]]))
return False
return True
栈的应用——迷宫问题
给一个二维列表,表示迷宫(0表示通道,1表示围墙)。给出算法,求一条走出迷宫的路径。
屏幕快照 2018-11-04 下午7.00.37.png
解决思路:
在一个迷宫节点(x,y)上,可以进行四个方向的探查:maze[x-1][y], maze[x+1][y], maze[x][y-1], maze[x][y+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 solve_maze(x1, y1, x2, y2):
stack = []
stack.append((x1,y1))
maze[x1][y1] = 2
while len(stack) > 0: # 当栈不空循环
cur_node = stack[-1]
if cur_node == (x2,y2): #到达终点
for p in stack:
print(p)
return True
for dir in dirs:# 四个方向探查
next_node = dir(*cur_node)
if maze[next_node[0]][next_node[1]] == 0: #找到一个能走的方向
stack.append(next_node)
maze[next_node[0]][next_node[1]] = 2 # 2表示已经走过的点
break
else: #如果一个方向也找不到
stack.pop()
else:
print("无路可走")
return False
----队列
队列(Queue)是一个数据集合,仅允许在列表的一端进行插入,另一端进行删除。
屏幕快照 2018-11-04 下午5.38.49.png
进行插入的一端称为队尾(rear),插入动作称为进队或入队
进行删除的一端称为队头(front),删除动作称为出队
队列的性质:先进先出(First-in, First-out)
双向队列:队列的两端都允许进行进队和出队操作。
队列能否简单用列表实现?为什么?
屏幕快照 2018-11-04 下午5.41.31.png
初步设想:
列表+两个下标指针
创建一个列表和两个变量,front变量指向队首,rear变量指向队尾。初始时,front和rear都为0。
进队操作:元素写到li[rear]的位置,rear自增1。
出队操作:返回li[front]的元素,front自减1。
通过简单的列表来实现队列,看结果的话可以实现,但是会有内存空间的浪费。
队列的实现原理——环形队列
屏幕快照 2018-11-04 下午5.44.12.png 环形队列:当队尾指针front == Maxsize + 1时,再前进一个位置就自动到0。
实现方式:求余数运算
队首指针前进1:front = (front + 1) % MaxSize
队尾指针前进1:rear = (rear + 1) % MaxSize
队空条件:rear == front
队满条件:(rear + 1) % MaxSize == front
python内置的队列模块
使用方法:from collections import deque
创建队列:queue = deque(li)
进队:append
出队:popleft
双向队列队首进队:appendleft
双向队列队尾进队:pop队列的实际应用
linux命令中的head(查看文件前几行)和tail(查看文件后几行)
队列解决迷宫问题
from collections import deque
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 solve_maze2(x1, y1, x2, y2):
queue = deque()
path = [] # 记录出队之后的节点
queue.append((x1, y1, -1))
maze[x1][y1] = 2
while len(queue) > 0:
cur_node = queue.popleft()
path.append(cur_node)
if cur_node[0] == x2 and cur_node[1] == y2: # 到终点
real_path = []
x, y, i = path[-1]
real_path.append((x, y))
while i >= 0:
node = path[I]
real_path.append(node[0:2])
i = node[2]
real_path.reverse()
for p in real_path:
print(p)
return True
for dir in dirs: # 四个方向探查
next_node = dir(cur_node[0], cur_node[1])
if maze[next_node[0]][next_node[1]] == 0: # 可以走
queue.append((next_node[0], next_node[1], len(path) - 1))
maze[next_node[0]][next_node[1]] = 2 # 标记为已经走过
else:
print("无路可走")
return False
----链表
链表中每一个元素都是一个对象,每个对象称为一个节点,包含有数据域key和指向下一个节点的指针next。通过各个节点之间的相互连接,最终串联成一个链表。
屏幕快照 2018-11-04 下午7.37.27.png
节点定义:
建立链表
头插法和尾插法 建立链表.GIF
头插法代码演示
class Node:
def __init__(self, data):
self.data = data
self.next = None
def create_linklist(li):
head = Node(None)# 头节点
for num in li:
node = Node(num)
node.next = head.next#先接后面的再接前面的,防止丢失数据
head.next = node
return head
def print_linklist(head):
node = head.next
while node:
print(node.data)
node = node.next
尾插法代码演示
class Node:
def __init__(self, data):
self.data = data
self.next = None
def create_linklist_tail(li):
head = Node(None) # 空的头节点
tail = head # 定义一个尾巴记录一下
for num in li:
node = Node(num)
tail.next = node # 链接新数据
tail = node # 更新尾巴
return head
def print_linklist(head):
node = head.next
while node:
print(node.data)
node = node.next
链表节点的插入和删除:
链表的插入和删除.GIF
插入:
p.next = curNode.next
curNode.next = p
删除:
p = curNode.next
curNode.next = curNode.next.next
del p
双向链表
双链表中每个节点有两个指针:一个指向后面节点、一个指向前面节点。
和链表的情况大体一致,当涉及到插入删除是记得画图,直观的保证数据的安全。
链表和顺序表的对比:
链表在插入和删除的操作上明显快于顺序表
链表的内存可以更灵活的分配
链表这种链式存储的数据结构对树和图的结构有很大的启发性
----哈希表
哈希表概念:
一个通过哈希函数来计算数据存储位置的数据结构,通常支持如下操作:
insert(key, value):插入键值对(key,value)-----键值就是字典,有键没值就是集合
get(key):如果存在键为key的键值对则返回其value,否则返回空值
delete(key):删除键为key的键值对
直接寻值表
屏幕快照 2018-11-04 下午8.45.48.png
当关键字的全域U比较小时,直接寻址是一种简单而有效的方法。
直接寻址技术缺点:
当域U很大时,需要消耗大量内存,很不实际
如果域U很大而实际出现的key很少,则大量空间被浪费
无法处理关键字不是数字的情况
哈希
直接寻址表:key为k的元素放到k位置上改进直接寻址表:哈希(Hashing)
构建大小为m的寻址表T
key为k的元素放到h(k)位置上----h为哈希函数
h(k)是一个函数,其将域U映射到表T[0,1,...,m-1]
哈希表
哈希表(Hash Table,又称为散列表),是一种线性表的存储结构。哈希表由一个直接寻址表和一个哈希函数组成。哈希函数h(k)将元素关键字k作为自变量,返回元素的存储下标。简单哈希函数:
除法哈希:h(k) = k mod m (取余数)
乘法哈希:h(k) = floor(m(kA mod 1)) 0<A<1假设有一个长度为7的数组,哈希函数h(k)=k mod 7。元素集合{14,22,3,5}的存储方式如下图。
屏幕快照 2018-11-04 下午8.54.26.png
哈希冲突
由于哈希表的大小是有限的,而要存储的值的总数量是无限的,因此对于任何哈希函数,都会出现两个不同元素映射到同一个位置上的情况,这种情况叫做哈希冲突。
比如:h(k)=k mod 7, h(0)=h(7)=h(14)=...
解决哈希冲突
开放寻址法:
开放寻址法:如果哈希函数返回的位置已经有值,则可以向后探查新的位置来存储这个值。
线性探查:如果位置i被占用,则探查i+1, i+2,……
二次探查:如果位置i被占用,则探查i+12,i-12,i+22,i-22,……
二度哈希:有n个哈希函数,当使用第1个哈希函数h1发生冲突时,则尝试使用h2,h3,……拉链法
拉链法
拉链法:哈希表每个位置都连接一个链表(链表的插入和查找的时间复杂度都是O(1)的),当冲突发生时,冲突的元素将被加到该位置链表的最后。
哈希表在Python中的应用
字典与集合都是通过哈希表来实现的。在Python中的字典:a = {'name': 'wangjifei', 'age': 27, 'gender': 'Man'}
使用哈希表存储字典,通过哈希函数将字典的键映射为下标。
假设h(‘name’) = 3, h(‘age’) = 1, h(‘gender’) = 4,则哈希表存储为[None, 27, None, ’wangjifei’, ‘Man’]
-----二叉树
二叉树的链式存储:
屏幕快照 2018-11-04 下午9.12.40.png
将二叉树的节点定义为一个对象,节点之间通过类似链表的链接方式来连接。
二叉树的遍历:
前序遍历(根--左--右):EACBDGF
中序遍历(左--根--右):ABCDEGF
后序遍历(左--右--根):BDCAFGE
层次遍历:EAGCFBD 屏幕快照 2018-11-04 下午9.14.28.png
有一种题是:给前中后三种遍历结果的其中两种顺序,画出二叉树来
解题思路:前序定根,中序分左右
二叉树的四种遍历代码实现:
from collections import deque
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='')
def level_order(root):
# 层次遍历(队列实现)
queue = deque()
queue.append(root)
while len(queue) > 0:
node = queue.popleft()
print(node.data, end='')
if node.lchild:
queue.append(node.lchild)
if node.rchild:
queue.append(node.rchild)
树还有二叉搜索树和AVL树,如果有想进一步了解的,自行网上查找资料吧!!!
网友评论