学习数据结构与算法的目的:
1.掌握底层 coding
2.从顶层宏观的去观察一种数据结构的各种操作 推荐 一个动态可视化网站 Visualgo
栈
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。栈允许进行插入和删除操作的一端称为栈顶(top),另一端为栈底(bottom);栈底固定,而栈顶浮动;栈中元素个数为零时称为空栈。插入一般称为进栈(PUSH),删除则称为退栈(POP)。由于堆叠数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。栈也称为后进先出表。
栈的复杂度
栈属于常见的一种线性结构,对于进栈和退栈而言,时间复杂度都为 O(1)
栈的基本功能实现
class Stack(object): #创建一个Stack类
def __init__(self, limit=10):
self.stack = [] #存放元素
self.limit = limit #栈容量极限
def push(self, data): #push进栈 判断栈是否溢出
if len(self.stack) >= self.limit:
print('*Error')
pass
self.stack.append(data)
def pop(self): #pop出栈
if self.stack:
return self.stack.pop()
else:
raise IndexError('pop from an empty stack') #空栈不能被弹出
def peek(self): #查看栈的最上面的元素
if self.stack:
return self.stack[-1]
def is_empty(self): #判断栈是否为空
return not bool(self.stack)
def size(self): #栈的大小
return len(self.stack)
给个例子,根据stack数据结构的特点,检查括号是否完全匹配
class Stack(object): #创建一个Stack类
def __init__(self, limit=10):
self.stack = [] #存放元素
self.limit = limit #栈容量极限
def push(self, data): #push进栈 判断栈是否溢出
if len(self.stack) >= self.limit:
print('*Error')
pass
self.stack.append(data)
def pop(self): #pop出栈
if self.stack:
return self.stack.pop()
else:
raise IndexError('pop from an empty stack') #空栈不能被弹出
def peek(self): #查看栈的最上面的元素
if self.stack:
return self.stack[-1]
def is_empty(self): #判断栈是否为空
return not bool(self.stack)
def size(self): #栈的大小
return len(self.stack)
def balanced_parentheses(parentheses):
stack = Stack(len(parentheses))
for parenthesis in parentheses:
if parenthesis == '(':
stack.push(parenthesis)
elif parenthesis == ')':
if stack.is_empty():
return False
stack.pop()
return stack.is_empty()
if __name__ == '__main__':
examples = ['((()))', '((())', '(()))']
print('Balanced parentheses demonstration:\n')
for example in examples:
print(example + ':' + str(balanced_parentheses(example)))
#输出
((())):True
((()):False
(())):False
链表
链表(linked_list)是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现,每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。根据指针的指向,链表能形成不同的结构,例如单链表,双向链表,循环链表。
链表通过将链点 i 与其邻居链点 i+1 通过指针相关联,从索引 0 到索引 N-1 对链点进行排序。链表分为单链表和双链表两种。
单链表的具体功能是如何实现的。
#单链表的具体功能
class Node: #创建一个 Node 的类,作为基础数据结构:链点,并初始化对应的内参
def __init__(self, data):
self.data = data #表示对应的元素值
self.next = None #表示下一个链接的链点
class Linked_List: #创建Linked_List类 并初始化对象的内参
def __init__(self):
self.head = None #表示链表的头部元素
def initlist(self, data_list): #链表初始化函数
self.head = Node(data_list[0]) #创建头结点
temp = self.head
for i in data_list[1:]: #逐个为data内的数据创建结点 建立链表
node = Node(i)
temp.next = node
temp = temp.next
def is_empty(self):
if self.head.next == None: #判断链表是否为空
print("Linked_list is empty")
return True
else:
return False
def insert(self, key, value): #链表中任意位置添加一个 item 元素 链表插入数据函数
if key<0 or key>self.get_length()-1:
print("insert error")
temp = self.head
i = 0
while i<=key: #遍历找到索引值为 key 的结点 在其后面插入结点
pre = temp
temp = temp.next
i = i+1
node = Node(value)
pre.next = node
node.next = temp
def remove(self, key): #链表删除数据函数
if key<0 or key>self.get_length()-1:
print("insert error")
i = 0
temp = self.head
while temp != None: #遍历找到索引值为 key 的结点
pre = temp
temp = temp.next
i = i+1
if i==key:
pre.next = temp.next
temp = None
return True
pre.next = None
def get_length(self): #获取链表的长度
temp = self.head #临时变量指向队列头部
length = 0 #计算链表的长度变量
while temp != None:
length = length +1
temp = temp.next
return length
def print_list(self): #遍历链表,并将元素依次打印出来
print("linked_list:")
temp = self.head
while temp is not None:
print(temp.data)
temp = temp.next
def reverse(self): #将链表反转
prev = None
current = self.head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
self.head = prev
复杂度
链表属于常见的一种线性结构,对于插入和移除而言,时间复杂度都为 O(1)
但是对于搜索操作而言,不管从链表的头部还是尾部,都需要遍历 O(n),所以最好复杂度为 O(1),最坏的情况就是从头部遍历到尾部才搜索出对应的元素,所以最坏复杂度为 O(n),平均复杂度为 O(n)。
归纳如下:
* 最好复杂度为 O(1)
* 最坏复杂度为 O(n)
* 平均复杂度为 O(n)
双向链表(Double_linked_list)也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。双链表具体实现代码:
class Node(object):
#双向链表节点
def __init__(self, item):
self.item = item
self.next = None
self.prev = None
class DLinkList(object):
#双向链表
def __init__(self):
self._head = None
def is_empty(self):
#判断链表是否为空
return self._head == None
def get_length(self):
#返回链表的长度
cur = self._head
count = 0
while cur != None:
count = count+1
cur = cur.next
return count
def travel(self):
#遍历链表
cur = self._head
while cur != None:
print(cur.item)
cur = cur.next
print("")
def add(self, item):
#头部插入元素
node = Node(item)
if self.is_empty():
#如果是空链表 将_head指向node
self._head = node
else:
#将node的next指向_head的头节点
node.next = self._head
#将_head的头节点的prev指向node
self._head.prev = node
#将_head指向node
self._head = node
def append(self, item):
#尾部插入元素
node = Node(item)
if self.is_empty():
#如果是空链表将_head指向node
self._head = node
else:
#移动到链表尾部
cur = self._head
while cur.next != None:
cur = cur.next
#将尾节点cur的next指向node
cur.next = node
#将node的prev指向cur
node.prev = cur
def search(self, item):
#查找元素是否存在
cur = self._head
while cur != None:
if cur.item == item:
return True
cur = cur.next
return False
def insert(self, pos, item):
#在指定位置添加节点
if pos <= 0:
self.add(item)
elif pos > (self.length()-1):
self.append(item)
else:
node = Node(item)
cur = self._head
count = 0
#移动到指定位置的前一个位置
while count < (pos - 1):
count += 1
cur = cur.next
#将node的prev指向cur
node.prev = cur
#将node的next指向cur的下一个节点
node.next = cur.next
#将cur的下一个节点的prev指向node
cur.next.prev = node
#将cur的next指向node
cur.next = node
def remove(self, item):
#删除元素
if self.is_empty():
return
else:
cur = self._head
if cur.item == item:
#如果首节点的元素即是要删除的元素
if cur.next == None:
#如果链接只有这一个节点
self._head = None
else:
#将第二个节点的prev设置为None
cur.next.prev = None
#将_head指向第二个节点
self._head = cur.next
return
while cur != None:
if cur.item == item:
#将cur的前一个节点的next指向cur的后一个节点
cur.prev.next = cur.next
#将cur的后一个节点的prev指向cur的前一个节点
cur.next.prev = cur.prev
break
cur = cur.next
举个列子,交换单链表里两个链点
class Node:
def __init__(self, data):
self.data = data
self.next = None
class Linkedlist:
def __init__(self):
self.head = None
def print_list(self):
print("linked_list:")
temp = self.head
new_list = []
while temp is not None:
new_list.append(temp.data)
temp = temp.next
print(new_list)
def insert(self, new_data):
new_node = Node(new_data)
new_node.next = self.head
self.head = new_node
def swapNodes(self, d1, d2):
prevD1 = None
prevD2 = None
if d1 == d2:
return
else:
D1 = self.head
while D1 is not None and D1.data != d1:
prevD1 = D1
D1 = D1.next
D2 = self.head
while D2 is not None and D2.data != d2:
prevD2 = D2
D2 = D2.next
if D1 is None and D2 is not None:
return
if prevD1 is not None:
prevD1.next = D2
else:
self.head = D2
if prevD2 is not None:
prevD2.next = D1
else:
self.head = D1
temp = D1.next
D1.next = D2.next
D2.next = temp
if __name__ == '__main__':
list = Linkedlist()
list.insert(5)
list.insert(4)
list.insert(3)
list.insert(2)
list.insert(1)
list.print_list()
list.swapNodes(1, 4)
print("After swampping")
list.print_list()
#输出
linked_list:
[1, 2, 3, 4, 5]
After swampping
linked_list:
[4, 2, 3, 1, 5]
队列
队列 (queue) 是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列符合先进先出[FIFO]的原则。因为要排队的第一个项目,最终将是第一个要出列的项目,如在现实生活中的队列,先来的站在队列前面,后来的就只能站在队列后面啦。
队列有两种实现形式,分为两种:数组和链表。
class Node(object):
def __init__(self, elem, next = None):
self.elem = elem #表示对应的元素值
self.next = next #表示下一个链接的链点
#创建Queue类 以链表形式的队列并初始化对应的内参
class Queue(object):
def __init__(self):
self.head = None #头部链点为None
self.rear = None #尾部链点为None
#添加is_empty函数 判断队列是否为空
def is_empty(self):
return self.head is None #判断队列是否为空
#添加enqueue(elem)函数 往队列中添加一个elem元素
def enqueue(self, elem):
p = Node(elem) #初始化一个新的点
if self.is_empty():
self.head = p #队列头部为新的链点
self.rear = p #队列尾部为新的链点
else:
self.rear.next = p #队列尾部的后继是这个新的点
self.rear = p #让队列尾部指针指向这个新的点
#添加dequeue函数 从队列头部删除一个元素
def dequeue(self):
if self.is_empty():
print("empty") #队列为空则退出dequeue操作
else:
result = self.head.elem #result为队列头部元素
self.head = self.head.next #改变队列头部指针位置
return result
#peek() 查看队列头部的元素
def peek(self):
if self.is_empty():
print("NOT_FOUND")
else:
return self.head.elem
#打印出队列的所有元素
def print_queue(self):
print("queue")
temp = self.head
myqueue = [] #暂存队列数据
while temp is not None:
myqueue.append(temp.elem)
temp = temp.next
print(myqueue)
class Queue():
def __init__(self):
self.entries = [] #表示队列内的参数
self.length = 0 #表示队列的长度
self.front = 0 #表示队列头部位置
def enqueue(self, item):
self.entries.append(item) #往队列里加元素
self.length = self.length + 1 #队列长度增1
def dequeue(self):
self.length = self.length - 1
dequeued = self.entries[self.front] #dequeued是队首元素
self.front -= 1 #队首位置减少1
self.entries = self.entries[self.front:] #队列元素更新为退队之后的队列
return dequeued
def peek(self):
return self.entries[0] #直接返回队列的队首元素
复杂度分析
队列属于常见的一种线性结构,对于出队和进队而言,时间复杂度都为 O(1)
树
树 (tree) 是一种非常高效的非线性存储结构。树,可以很形象的理解,有根,有叶子,对应在数据结构中就是根节点、叶子节点,同一层的叶子叫兄弟节点,邻近不同层的叫父子节点。
* 二叉树,就是每个节点都至多有二个子节点的树。
* 满二叉树,就是除了叶子节点外,每个节点都有左右两个子节点,这种二叉树叫做满二叉树。
* 完全二叉树,就是叶子节点都在最底下两层,最后一层叶子节都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
二叉树的具体功能实现
1. 先定义一个节点 node 类,存储数据 data 和左子节点 left 以及 右子节点 right。
2. 再实现二叉树 binary_tree 的类,应至少有以下属性和函数: 属性:有一个根节点(root) , 它是 node 类。 函数:添加子节点 add ,返回父节点 get_parent,删除子节点 delete。
class Node(object): #创建Node类
def __init__(self, item):
self.item = item #表示对应的元素
self.left = None #表示左子节点
self.right = None #表示右子节点
def __str__(self):
return str(self.item) #print 一个 Node 类时会打印 __str__ 的返回值
class Tree(object): #创建Tree类 定义根节点
def __init__(self):
self.root = Node('root') #根节点定义为 root 永不删除,作为哨兵使用
def add(self, item): #add() 添加子节点到树里
node = Node(item)
if self.root is None: #如果二叉树为空,那么生成的二叉树最终为新插入树的点
self.root = node
else:
q = [self.root] # 将q列表,添加二叉树的根节点
while True:
pop_node = q.pop(0)
if pop_node.left is None: #左子树为空则将点添加到左子树
pop_node.left = node
return
elif pop_node.right is None: #右子树为空则将点添加到右子树
pop_node.right = node
return
else:
q.append(pop_node.left)
q.append(pop_node.right)
def get_parent(self, item): #找到item的父节点
if self.root.item == item:
return None # 根节点没有父节点
tmp = [self.root] # 将tmp列表,添加二叉树的根节点
while tmp:
pop_node = tmp.pop(0)
if pop_node.left and pop_node.left.item == item: #某点的左子树为寻找的点
return pop_node #返回某点,即为寻找点的父节点
if pop_node.right and pop_node.right.item == item: #某点的右子树为寻找的点
return pop_node #返回某点,即为寻找点的父节点
if pop_node.left is not None: #添加tmp 元素
tmp.append(pop_node.left)
if pop_node.right is not None:
tmp.append(pop_node.right)
return None
def delete(self, item):
if self.root is None:
return False
parent = self.get_parent(item)
if parent:
del_node = parent.left if parent.left.item == item else parent.right #待删除节点
if del_node.left is None:
if parent.left.item == item:
parent.left = del_node.right
else:
parent.right = del_node.right
del del_node
return True
elif del_node.right is None:
if parent.left.item == item:
parent.left = del_node.left
del del_node
return True
else:
tmp_pre = del_node
tmp_next = del_node.right
if tmp_next.left is None:
tmp_pre.right = tmp_next.right
tmp_next.left = del_node.left
tmp_next.right = del_node.right
else:
while tmp_next.left:
tmp_pre = tmp_next
tmp_next = tmp_next.left
tmp_pre.left = tmp_next.right
tmp_next.left = del_node.left
tmp_next.right = del_node.right
if parent.left.item == item:
parent.left = tmp_next
else:
parent.right = tmp_next
del del_node
return True
else:
return False
二叉树的遍历:是指从根结点出发,按照某种次序依次访问二叉树中的所有结点,使得每个结点被访问一次且仅被访问一次。这里有两个关键词:访问和次序。访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计算,输出打印等。它算作是一个抽象操作。
二叉树的遍历次序不同于线性结构,最多也就是从头到尾、循环和双向等简单的遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。
二叉树的遍历方式,主要分为三种:
1. 中序遍历
2. 后序遍历
3. 前序遍历
中序遍历: 先中序访问左子树,然后访问根,最后中序访问右子树。总的来说是左—根—右
1. 先处理左子树,然后处理当前节点,再处理右子树;
2. 对于一颗二叉查找树,所有的信息都是有序排列的,中序遍历可以是信息有序输出,且运行时间为 O(n);
3. 递归实现中序遍历。
def inorder(self, node):
if node is None:
return []
result = [node.item]
left_item = self.inorder(node.left)
right_item = self.inorder(node.right)
return left_item + result + right_item
后序遍历: 先后序访问左子树,然后后序访问右子树,最后访问根。总的来说是左—右—根
1. 先处理左右子树,然后再处理当前节点,运行时间为 O(n)
2. 递归实现后序遍历
def postorder(self, node):
if node is None:
return []
result = [node.item]
left_item = self.postorder(node.left)
right_item = self.postorder(node.right)
return left_item + right_item + result
前序遍历: 先访问根节点,再先序遍历左子树,然后再先序遍历右子树。总的来说是根—左—右
1. 先处理当前节点,再处理左右子树;
2. 递归实现先序遍历。
def preorder(self, node):
if node is None:
return []
result = [node.item]
left_item = self.preorder(node.left)
right_item = self.preorder(node.right)
return result + left_item + right_item
字典树
字典树,又称单词查找树,Trie 树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
字典树的主要性质:
1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
3. 每个节点的所有子节点包含的字符都不相同。
class TrieNode:
def __init__(self):
self.nodes = dict() #构建字典
self.is_leaf = False
def insert(self, word: str): #插入一个字到字典树中
curr = self
for char in word:
if char not in curr.nodes:
curr.nodes[char] = TrieNode()
curr = curr.nodes[char]
curr.is_leaf = True
def insert_many(self, words: [str]): #插入一列表的字到字典树中
for word in words:
self.insert(word)
def search(self, word: str): #在字典树里面查询一个字
curr = self
for char in word:
if char not in curr.nodes:
return False
curr = curr.nodes[char]
return curr.is_leaf
字典树的应用: 用在统计和排序大量字符串,如自动机。字典树能做前缀搜索,在正则匹配,数据压缩,构建索引都可能用到。
堆
堆 (heap) 是一种经过排序的完全二叉树,其中任一非叶子节点的值均不大于(或不小于)其左孩子和右孩子节点的值。堆,又被称为优先队列(priority queue)。尽管名为优先队列,但堆并不是队列。
* 最大堆 根结点的键值是所有堆结点键值中最大者。
* 最小堆 根结点的键值是所有堆结点键值中最小者
基本功能实现——堆有两点需要了解,一是堆一般采用完全二叉树;二是堆中的每一个节点都大于其左右子节点(大顶堆),或者堆中每一个节点都小于其左右子节点(小顶堆)。
class heap(object):
def __init__(self):
self.data_list = [] #初始化一个空堆,使用数组来存放堆元素,节省存储
def get_parent_index(self, index):
#返回父节点的下标
if index == 0 or index > len(self.data_list) - 1:
return None
else:
return (index - 1) >> 1
def swap(self, index_a, index_b):
#交换数组中的两个元素
self.data_list[index_a], self.data_list[index_b] = self.data_list[index_b], self.data_list[index_a]
def insert(self, data):
#先把元素放在最后,然后从后往前依次堆化
#这里以大顶堆为例,如果插入元素比父节点大,则交换,直到最后
self.data_list.append(data)
index = len(self.data_list) - 1
parent = self.get_parent_index(index)
#循环,直到该元素成为堆顶,或小于父节点(对于大顶堆)
while parent is not None and self.data_list[parent] < self.data_list[index]:
self.swap(parent, index)
index = parent
parent = self.get_parent_index(parent)
def removeMax(self):
#删除堆顶元素,然后将最后一个元素放在堆顶,再从上往下依次堆化
remove_data = self.data_list[0]
self.data_list[0] = self.data_list[-1]
del self.data_list[-1]
#堆化
self.heapify(0)
return remove_data
def heapify(self, index):
total_index = len(self.data_list) - 1
while True:
maxvalue_index = index
if 2*index + 1 <= total_index and self.data_list[2*index+1] > self.data_list[maxvalue_index]:
maxvalue_index = 2*index + 1
if 2*index + 2 <= total_index and self.data_list[2*index+2] > self.data_list[maxvalue_index]:
maxvalue_index = 2*index + 2
if maxvalue_index == index:
break
self.swap(index, maxvalue_index)
index = maxvalue_index
举例,将元素 1-10 放进堆,并展示建堆情况,及删除堆顶元素情况
class heap(object):
def __init__(self):
self.data_list = [] #初始化一个空堆,使用数组来存放堆元素,节省存储
def get_parent_index(self, index):
#返回父节点的下标
if index == 0 or index > len(self.data_list) - 1:
return None
else:
return (index - 1) >> 1
def swap(self, index_a, index_b):
#交换数组中的两个元素
self.data_list[index_a], self.data_list[index_b] = self.data_list[index_b], self.data_list[index_a]
def insert(self, data):
#先把元素放在最后,然后从后往前依次堆化
#这里以大顶堆为例,如果插入元素比父节点大,则交换,直到最后
self.data_list.append(data)
index = len(self.data_list) - 1
parent = self.get_parent_index(index)
#循环,直到该元素成为堆顶,或小于父节点(对于大顶堆)
while parent is not None and self.data_list[parent] < self.data_list[index]:
self.swap(parent, index)
index = parent
parent = self.get_parent_index(parent)
def removeMax(self):
#删除堆顶元素,然后将最后一个元素放在堆顶,再从上往下依次堆化
remove_data = self.data_list[0]
self.data_list[0] = self.data_list[-1]
del self.data_list[-1]
#堆化
self.heapify(0)
return remove_data
def heapify(self, index):
total_index = len(self.data_list) - 1
while True:
maxvalue_index = index
if 2*index + 1 <= total_index and self.data_list[2*index+1] > self.data_list[maxvalue_index]:
maxvalue_index = 2*index + 1
if 2*index + 2 <= total_index and self.data_list[2*index+2] > self.data_list[maxvalue_index]:
maxvalue_index = 2*index + 2
if maxvalue_index == index:
break
self.swap(index, maxvalue_index)
index = maxvalue_index
if __name__ == "__main__":
myheap = heap()
for i in range(10):
myheap.insert(i+1)
print('建堆:', myheap.data_list)
print('删除堆顶元素:', [myheap.removeMax() for _ in range(10)])
#输出
建堆: [10, 9, 6, 7, 8, 2, 5, 1, 4, 3]
删除堆顶元素: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
图
图的分类
* 无向图 图是若干个顶点(Vertices)和边(Edges)相互连接组成的。边仅由两个顶点连接,并且没有方向的图称为无向图。
* 有向图 在有向图中,边是单向的:每条边连接的两个顶点都是一个有序对,它们的邻接性是单向的。我们开发过程中碰到的很多场景都是有向图:比如任务调度的依赖关系,社交网络的任务关系等等都是天然的有向图。
图的实现——表示图通常有四种方法:数组表示法、邻接表、十字链表和邻接多重表。邻接表是图的一种链式存储结构,十字链表是有向图的另一种链式存储结构,邻接多重表是无向图的另一种链式存储结构。这里主要讲解一下邻接表的表示和实现,邻接表中有两种结点,一种是头结点,另一种是表结点,头结点中存储一个顶点的数据和指向链表中第一个结点,表结点中存储当前顶点在图中的位置和指向下一条边或弧的结点,表头结点用链式或顺序结构方式存储。
无向图的实现
#图的邻接表实现
class Graph(object) :
def __init__(self):
self.nodes = [] #表示图的点集
self.edge = {} #表示图的边集
def insert(self, a, b):
if not(a in self.nodes): #如果a 不在图的点集里,则添加a
self.nodes.append(a)
self.edge[a] = list()
if not(b in self.nodes):
self.nodes.append(b)
self.edge[b] = list()
self.edge[a].append(b) #a连接b
self.edge[b].append(a) #b连接a
def succ(self, a):
return self.edge[a] #返回与a连接的点
def show_nodes(self):
return self.nodes #返回图的点集
def show_edge(self):
print(self.edge)
if __name__ == "__main__":
graph = Graph()
graph.insert('0', '1')
graph.insert('0', '2')
graph.insert('0', '3')
graph.insert('1', '3')
graph.insert('2', '3')
graph.show_edge()
#输出 graph的存储形式
{'0': ['1', '2', '3'], '1': ['0', '3'], '2': ['0', '3'], '3': ['0', '1', '2']}
邻接矩阵表示法,用两个数组表示,一个一维数组和一个二维数组。
一维数组存储节点信息,二维数组存储节点之间的关系。
#图的邻接矩阵实现
class Graph:
def __init__(self, vertex):
self.vertex = vertex
self.graph = [[0]*vertex for i in range(vertex)]
def insert(self, u, v):
self.graph[u - 1][v - 1] = 1 #对存在连接关系的两个点,在矩阵里置1代表存在连接关系,没有连接关系则置0
self.graph[v - 1][u - 1] = 1
def show(self): #展示图
for i in self.graph:
for j in i:
print(j, end=' ')
print(' ')
if __name__ == "__main__":
graph = Graph(5)
graph.insert(1, 4)
graph.insert(4, 2)
graph.insert(4, 5)
graph.insert(2, 5)
graph.insert(5, 3)
graph.show()
#输出
0 0 0 1 0
0 0 0 1 1
0 0 0 0 1
1 1 0 0 1
0 1 1 1 0
图的遍历问题
通常图的遍历有两种:深度优先搜索和广度优先搜索。
深度优先搜索
深度优先搜索(DFS) 是树的先根遍历的推广,它的基本思想是:
从图 G 的某个顶点 v0 出发,访问 v0,然后选择一个与 v0 相邻且没被访问过的顶点 vi 访问,再从 vi 出发选择一个与 vi 相邻且未被访问的顶点 vj 进行访问,依次继续。如果当前被访问过的顶点的所有邻接顶点都已被访问,则退回到已被访问的顶点序列中最后一个拥有未被访问的相邻顶点的顶点 w,从 w 出发按同样的方法向前遍历,直到图中所有顶点都被访问。
#深度优先遍历
def dfs(graph, start):
explored, stack = [], [start] #explored:已经遍历的节点列表,stack:寻找待遍历的节点栈
explored.append(start)
while stack:
v = stack.pop()
for w in graph[v]:
if w not in explored:
explored.append(w)
stack.append(w)
return explored
G = {'0':['1'],
'1':['3'],
'2':['1'],
'3':['2','4'],
'4':['5'],
'5':['7'],
'6':['4'],
'7':['6']
}
print(dfs(G, '0'))
#输出
['0', '1', '3', '2', '4', '5', '7', '6']
图的最短路径问题
最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。给定一个图,和一个源顶点 src,找到从 src 到其它所有所有顶点的最短路径,图中可能含有负权值的边。在这里我们介绍两种常见的求解最短路径问题的算法。
Dijkstra 的算法是一个贪婪算法,时间复杂度是 O(VLogV)(使用最小堆)。但是迪杰斯特拉算法在有负权值边的图中不适用,Bellman-Ford 适合这样的图。在网络路由中,该算法会被用作距离向量路由算法。
Bellman-Ford 也比迪杰斯特拉算法更简单和同时也适用于分布式系统。但 Bellman-Ford 的时间复杂度是 O(VE),这要比迪杰斯特拉算法慢。(V 为顶点的个数,E 为边的个数)。
Dijkstra 算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块,Dijkstra 算法不能处理包含负边的图!
#Dijkstra 算法
import heapq
def dijkstra(graph, start, end):
heap = [(0, start)] #cost from start node, end node
visited = []
while heap:
(cost, u) = heapq.heappop(heap)
if u in visited:
continue
visited.append(u)
if u == end:
return cost
for v, c in G[u]:
if v in visited:
continue
next = cost +c
heapq.heappush(heap, (next, v))
return (-1, -1)
G = {'0':[['1', 2], ['2', 5]],
'1':[['0', 2], ['3', 3], ['4', 1]],
'2':[['0', 5], ['5', 3]],
'3':[['1', 3]],
'4':[['1', 1], ['5', 3]],
'5':[['2', 3], ['4', 3]]
}
shortDistance = dijkstra(G, '4', '2')
print(shortDistance)
并查集
并查集是一种数据结构,用于处理对 N 个元素的集合划分和判断是否属于同集合的问题。让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。
注:定义来自百度百科。并查集的主要性质
用集合中的某个元素来代表这个集合,该元素称为集合的代表元。一个集合内的所有元素组织成以代表元为根的树形结构。对于每一个元素 parent[x]指向 x 在树形结构上的父亲节点。如果 x 是根节点,则令 parent[x] = x。对于查找操作,假设需要确定 x 所在的的集合,也就是确定集合的代表元。可以沿着 parent[x]不断在树形结构中向上移动,直到到达根节点。判断两个元素是否属于同一集合,只需要看他们的代表元是否相同即可。
并查集的应用
1、维护无向图的连通性。支持判断两个点是否在同一连通块内,和判断增加一条边是否会产生环。
2、用在求解最小生成树的 Kruskal 算法里。
并查集的功能实现
#创建一个 union_find 的类,并初始化。初始化两个字典,一个保存节点的父节点,另外一个保存父节点的大小。初始化的时候,将节点的父节点设为自身,size 设为 1
class union_find(object):
def __init__(self, data_list):
self.father_dict = {}
self.size_dict = {}
for node in data_list:
self.father_dict[node] = node
self.size_dict[node] = 1
#用递归的方式查找父节点
def find(self, node):
father = self.father_dict[node]
if(node != father): #递归查找父节点
father = self.find(father)
self.father_dict[node] = father #在查找父节点的时候,顺便把当前节点移动到父节点上面这个操作算是一个优化
return father
#查看两个节点是不是在一个集合里面。通过调用 find 函数,判断两个节点是否是同一个父节点,如果是则判断两个节点属于一个集合。
def is_same_set(self, node_a, node_b):
return self.find(node_a) == self.find(node_b)
#将两个集合合并在一起
def union(self, node_a, node_b):
if node_a is None or node_b is None: #对合并的两个节点做初步判断,判断是否为空
return
a_head = self.find(node_a) #分别查找两个节点的父节点
b_head = self.find(node_b)
if(a_head != b_head): #当两个节点的父节点不一样时,才能做合并操作
a_set_size = self.size_dict[a_head]
b_set_size = self.size_dict[b_head]
if(a_set_size >= b_set_size):
self.father_dict[b_head] = a_head
self.size_dict[a_head] = a_set_size +b_set_size
else:
self.father_dict[a_head] = b_head
self.size_dict[b_head] = a_set_size +b_set_size
if __name__ == "__main__":
a = [1,2,3,4,5]
union_find = union_find(a)
union_find.union(1,2)
union_find.union(3,5)
union_find.union(3,1)
print(union_find.is_same_set(2,5))
#输出
True