算法

时间复杂度的计算

1) 一层循环:

1.列出循环函数 t 和每轮循环 i 的变化值

2.找到t与i之间的关系

3.确定循环停止的条件

4.联立两式,解方程

2)两层循环:

1.列出外层循环中 i 的变化值

2.列出内层语句的执行次数

3.求和,写结果

3)多层循环:

法一:

1.抽象为计算三维体积

法二:

2.列式求和

链表

package com.zfc.smallchange.oop;

public class LinkedList {
    public static void main(String[] args) {
        Node jack = new Node("jack");
        Node tom = new Node("tom");
        Node zfc = new Node("zfc");

        jack.next = tom;
        tom.pre = jack; // 修正前驱节点的链接
        tom.next = zfc;
        zfc.pre = tom;

        Node first = jack;
        // 打印链表
        while (first != null) {
            System.out.println(first.string()); // 调用 Node 类中的 string 方法打印节点数据
            first = first.next;
        }
        System.out.println("=======从尾部开始遍历链表======");
        // 从尾部开始遍历链表
        Node last = zfc;
        while (last != null) {
            System.out.println(last.string());
            last = last.pre;
        }
    }

    static class Node {
        public Object data;
        public Node pre;
        public Node next;

        public Node(Object data) {
            this.data = data;
        }

        // 新增方法 string
        public String string() {
            return "Node data: " + data;
        }
    }
}

数据结构(Python)

算法和算法分析

时间复杂度

空间复杂度

线性表的定义

选择排序

插入排序

快速排序

[50, 24, 60,93,24,20]
def partition(nums, low, high):
    # 进行分区操作,选取第一个值为基准
    pivot = nums[low]
    i = low
    j = high
    while i < j:
        # j是从右向左走,如果值大于pivot则位置保持不变,j左移
        while i < j and nums[j] >= pivot:
            j -= 1
        # 不满足上述条件时,nums[j]<pivot,应该放在左边,所以将i位置赋值为j
        # 此时j位置空出
        nums[i] = nums[j]
        # I是从左向右走,如果值小于pivot则位置保持不变,i右移
        while i < j and nums[i] < pivot:
            i += 1
        # 不满足上述条件时,nums[i]>=pivot,应该放在右边,所以将h位置赋值为i
        # 此时j位置空出
        nums[j] = nums[i]
    # 将pivot的值放到正确的索引位置
    nums[i] = pivot
    return  i

# 快速排序函数
def quickSort(arr, low, high):
    # arr[] --> 排序数组
    # low  --> 起始索引
    # high  --> 结束索引
    if low < high:
        pi = partition(arr, low, high)   #pi为基准值的正确索引位置
        quickSort(arr, low, pi - 1)  #递归的排序子序列
        quickSort(arr, pi + 1, high) #递归的排序子序列
    return arr

最优时间复杂度 : O(nlogn) 最坏时间复杂度 : O(n^2) 不稳定

归并排序

#merge的功能是将前后相邻的两个有序表归并为一个有序表的算法。
def merge(left, right):
    ll, rr = 0, 0
    result = []
    while ll < len(left) and rr < len(right):
        if left[ll] < right[rr]:
            result.append(left[ll])
            ll += 1
        else:
            result.append(right[rr])
            rr += 1
    result+=left[ll:]
    result+=right[rr:]
    return result

def merge_sort(alist):
    if len(alist) <= 1:
        return alist
    num = len(alist) // 2   # 从中间划分两个子序列
    left = merge_sort(alist[:num]) # 对左侧子序列进行递归排序
    right = merge_sort(alist[num:]) # 对右侧子序列进行递归排序
    return merge(left, right) #归并

tesl=[1,3,45,23,23,12,43,45,33,21]
print(merge_sort(tesl))
#[1, 3, 12, 21, 23, 23, 33, 43, 45, 45]

最优和最坏 : 时间复杂度 O(nlogn) 稳定

顺序查找

二分查找

递归方式实现

   def binary_search(alist, item):
       if len(alist) == 0:
           return False
       else:
           midpoint = len(alist)//2
           if alist[midpoint]==item:
             return True
           else:
             if item<alist[midpoint]:
               return binary_search(alist[:midpoint],item)
             else:
               return binary_search(alist[midpoint+1:],item)
   
   testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
   print(binary_search(testlist, 3))
   print(binary_search(testlist, 13))

非递归的方式实现

def binary_search(alist, item):
      first = 0
      last = len(alist)-1
      while first<=last:
          midpoint = (first + last)/2
          if alist[midpoint] == item:
              return True
          elif item < alist[midpoint]:
              last = midpoint-1
          else:
              first = midpoint+1
    return False
testlist = [0, 1, 2, 8, 13, 17, 19, 32, 42,]
print(binary_search(testlist, 3))
print(binary_search(testlist, 13))

数据结构

顺序表

链表

单向链表

#定义节点
class Node(object):
    def __init__(self,elem):
        self.elem=elem
        self.next=None
#构造单向链表
class SingleLinkedList:
    #判断链表是否为空:_head指向None,则为空
    def is_empty(self):
        '''
        if self._head==None:
            print("True")
        else:
            print("False")
            '''
        return self._head==None
    #单向链表初始化
    def __init__(self,node=None):
        #判断node是否为空
        if node!=None:
            headNode=Node(node)
            self._head=headNode
        else:
            self._head=node
    #计算链表长度
    def length(self):
        count=0
        curNode=self._head
        while curNode!=None:
            count+=1
            curNode=curNode.next
            return count
    #遍历链表
    def travel(self):
        curNode=self._head
        while curNode!=None:
            print(curNode.elem,end='\t')
            curNode=curNode.next
        print("  ")
    #在头部添加元素
    def add(self,item):
        #将传入的值构造成节点
        node=Node(item)
        #将新节点的链接域next指向头节点
        node.next=self._head
        #将链表的头_head指向新节点
        self._head=node
    #在尾部添加元素
    def append(self,item):
        #将传入的值构造成节点
        node=Node(item)
        if self.is_empty(): #单链表为空
            self._head=node
        else: #单链表不为空
            curNode=self._head
            while curNode.next!=None:
                curNode=curNode.next
            #修改节点,最后一个节点的next指向node
            curNode.next=node
 
    #在指定位置添加元素
    def insert(self,pos,item):
        #如果pos<=0,默认插入到头部
        if pos<=0:
            self.add(item)
        #如果pos>链表长度,直接在尾部追加
        elif pos>(self.length()-1):
            self.append(item)
        else:
            #构造节点
            node=Node(item)
            count=0
            preNode=self._head
            while count<(pos-1): #查找前一个节点
                count+=1
                preNode=preNode.next
            #修改指向
            #将前一个节点的next指向插入节点
            node.next=preNode.next
            #将插入位置的前一个节点next指向新节点
            preNode.next=node
 
    #查找节点是否存在
    def search(self,item):
        curNode=self._head
        while curNode!=None:
            if curNode.elem==item:
                return True
            curNode=curNode.next
        return False
    #删除节点
    def remove(self,item):
        curNode=self._head
        preNode=None
        while curNode!=None:
            if curNode.elem==item:
                #判断是否是头节点
                if preNode==None:#是头节点
                    self._head=curNode.next
                else:
                    #删除
                    preNode.next=curNode.next
            break
        else:
                preNode=curNode
                curNode=curNode.next
if __name__=="__main__":
    #初始化元素值为10单向链表
    singleLinkedList=SingleLinkedList(30)
    print("是否为空链表:",singleLinkedList.is_empty())
    print("链表长度为:",singleLinkedList.length())
    print("----遍历链表----")
    singleLinkedList.travel()
    print("-----查找-----",singleLinkedList.search(30))
    print("-----头部插入-----")
    singleLinkedList.add(1)
    singleLinkedList.add(2)
    singleLinkedList.add(3)
    singleLinkedList.travel()
    print("-----尾部追加-----")
    singleLinkedList.append(10)
    singleLinkedList.append(20)
    singleLinkedList.travel()
    print("-----链表长度-----",singleLinkedList.length())
    print("-----指定位置插入-----")
    singleLinkedList.insert(-1,100)
    singleLinkedList.travel()
    print("-----删除节点-----")
    singleLinkedList.remove(100)
    singleLinkedList.travel()

顺序表与链表的对比

双向链表

class Node(object):
    def __init__(self, item=None): 
        self.elem = item
        self.prev = None
        self.next = None

class DoubleLinkList:
    def __init__(self, node=None):
        self.__head = node


class DoubleLinkList:
    def __init__(self, node=None):
        self.__head = node
 
    def is_empty(self):  # 链表判空
        if self.__head is None:
            return True
        else:
            return False
 
    def length(self):  # 链表长度
        cur = self.__head
        count = 0  
        while cur is not None:
            count += 1
            cur = cur.next
        return count
 
    def travel(self):  # 遍历链表
        cur = self.__head
        while cur is not None:
            print(cur.elem, end=" ")
            cur = cur.next
        print(" ")
 
    def add(self, item):  # 链表头部添加元素
        # item 是元素
        node = Node(item)
        self.__head.prev = node
        node.next = self.__head
        self.__head = node
 
    def append(self, item):  # 链表尾部添加元素       
        node = Node(item)
        if self.is_empty():
            self.__head = node
        else:
            cur = self.__head
            while cur.next is not None:
                cur = cur.next
            node.prev = cur
            cur.next = node
 
    def insert(self, pos, item):  # 指定位置添加元素
        # pos 的位置从0起
        if pos <= 0:  # 头部插入
            self.add(item)
        elif pos > (self.length() - 1):  # 尾部插入
            self.append(item)
        else:  # 中间插入
            index = 0
            cur = self.__head
            while index < pos:
                index += 1
                cur = cur.next
            # 循环结束,cur在pos
            node = Node(item)
            node.prev = cur.prev
            node.next = cur
            node.prev.next = node  # 等价与 cur.prev.next=node
            node.next.prev = node  # 等价与 cur.prev=node
 
    def remove(self, item):  # 删除节点
        cur = self.__head
        while cur:
            if cur.elem == item:
                if cur == self.__head:  # 删除为头结点
                    self.__head = cur.next
                    if cur.next:
                        cur.next.prev = None
                else:  # 不在头结点
                    cur.prev.next = cur.next
                    if cur.next:
                        cur.next.prev = cur.prev
                return True
            else:
                cur = cur.next
        return False
 
    def search(self, item):  # 查找节点是否存在
        if self.is_empty():
            return False
        else:
            cur = self.__head
            while cur is not None:
                if cur.elem == item:
                    return True
                else:
                    cur = cur.next
            return False

#栈对象
class Stack(object):
    #创建一个空栈
    def __init__(self,size,top=-1):
        self.size=size #栈的大小
        self.top=top    #栈的栈顶,初始化为-1
        self.stackdata=[] #空列表,作为数据容器
    #判断这个栈是否满了
    def isfull(self):
        return self.top+1==self.size 
    #判断在栈是否为空栈
    def isEmpty(self):
        return self.top==-1
    #压栈,放入数据
    def push(self,val):
        if self.isfull(): #要做判断这个栈满了没有
            print('stack is full')
            return
        else:
            self.stackdata.append(val)
            self.top+=1
    #出栈,取出数据
    def pop(self):
        if self.isEmpty():#要判断这个栈是否为空
            print('empty')
            return
        else:
            self.stackdata.pop(self.top)
            self.top-=1
    #展示栈数据(也就是输出列表)
    def showstack(self):
        print(self.stackdata)
    #获取到栈顶的数据
    def peek(self):
        if self.top!=-1:
            return self.stackdata[self.top]
    #返回这个栈多少个数据项
    def size(self):
        return len(self.stackdata)

队列

class myQueue():

    def __init__(self, size):#初始化一个空队列
        self.size = size
        self.front =0
        self.rear = 0
        self.queue = []

    def enqueue(self, x):  # 入队操作
        if self.isfull():
            print("queue is full")
            return False
        else:
            self.queue.append(x)
            self.rear = self.rear + 1
            return True

    def dequeue(self):  # 出队操作
        if self.isempty():
            print("queue is empty")
            return False
        else:
            duiwei = self.queue[self.front]
            self.queue.pop(self.front)
            return duiwei

    def isfull(self):
        return self.rear - self.front + 1 == self.size
    def isempty(self):
        return self.front == self.rear

    def gethead(self):
        if self.isempty():
            return False
        else:
            return self.queue[self.front]

    def show(self):
        print(self.queue)

qq= myQueue(10)
print("初始化队列为空",qq.isempty())

for i in range(10):
    qq.enqueue(i)
qq.show()
print("插入十个元素后,队满",qq.isfull())
print(qq.dequeue())
print(qq.dequeue())
print(qq.dequeue())
print(qq.dequeue())

二叉树

二叉树的遍历

# 二叉树基于Python语言面自建类的实现

class Node(object):
    """
    节点类
    """
    def __init__(self, data, left=None, right=None):
        self.data = data
        self.left = left
        self.right = right


class BinTreeClassWay(object):
    """
    二叉树类
    """
    def __init__(self, root=None):
        self.root = root
    
    def is_empty(self):
        """
        判断二叉树是否为空
        """
        return self.root == None

    def add(self, data):
        """
        添加节点,此方法无法控制将节点挂在到那颗子树,也就意味着它最终形成的二叉树为完全二叉树或满二叉树
        """
        # 实例化一个新的节点对象
        node = Node(data)
        # 如果树是空的,则对根节点数据元素赋值
        if self.root == None:
            self.root = node
        else:
            # 创建一个存放节点的队列
            queue = []
            queue.append(self.root)
            # 对已有的节点按层次进行遍历
            while queue:
                # 弹出队列的第一个节点
                head = queue.pop(0)
                if head.left == None:
                    head.left = node
                    return
                elif head.right == None:
                    head.right = node
                    return
                else:
                    #如果左右子树都不为空,加入队列继续判断
                    queue.append(head.left)
                    queue.append(head.right)

    # 基于深度优先遍历
    def preorder(self, bin_tree):
        """
        先序遍历方法: 根节点->左子树->右子树
        """
        if bin_tree == None:
            return
        print(bin_tree.data)
        self.preorder(bin_tree.left)
        self.preorder(bin_tree.right)
    
    # 基于深度优先遍历
    def inorder(self, bin_tree):
        """
        中序遍历方法: 左子树->根节点->右子树
        """
        if bin_tree == None:
            return
        self.preorder(bin_tree.left)
        print(bin_tree.data)
        self.preorder(bin_tree.right)
    
    # 基于深度优先遍历
    def postorder(self, bin_tree):
        """
        后序遍历方法: 左子树->右子树->根节点
        """
        if bin_tree == None:
            return
        self.preorder(bin_tree.left)
        self.preorder(bin_tree.right)
        print(bin_tree.data)

    # 基于广度优先遍历
    def breadth_first_travel(self, bin_tree):
        """
        广度优先遍历
        """
        if bin_tree == None:
            return
        # 该方法和 add 方法中的插入元素类似
        queue = []
        queue.append(bin_tree)
        while queue:
            node = queue.pop(0)
            print(node.data)
            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)

    def show(self):
        """
        返回二叉树
        """
        return self.root

if __name__ == "__main__":
    bin_tree = BinTreeClassWay()
    bin_tree.add('A')
    bin_tree.add('B')
    bin_tree.add('C')
    bin_tree.add('D')
    bin_tree.add('E')
    bin_tree.add('F')
    bin_tree.add('G')
    print('先序遍历')
    bin_tree.preorder(bin_tree.show())
    print('中序遍历')
    bin_tree.inorder(bin_tree.show())
    print('后序遍历')
    bin_tree.postorder(bin_tree.show())
    print('广度优先遍历')
    bin_tree.breadth_first_travel(bin_tree.show())

一、图Graph的概念

图Graph是比树更为一般的结构, 也是由节点和边构成

实际上树是一种具有特殊性质的图

图可以用来表示现实世界中很多事物

道路交通系统、航班线路、互联网连接、或者是大学中课程的先修次序

一旦我们对图相关问题进行了准确的描述,就可以采用处理图的标准算法来解决那些

看起来很艰深的问题

1.1 互联网

互联网是一张百亿个信息点的巨型网络

提供内容的Web站点已突破10亿个

由超链接相互连接的网页更是不计其数

Google每天处理的数据量约10PB

在天文数字规模的网络面前人脑已经无法处理

1.2 社交网络:六度分隔理论

世界上任何两个人之间通过最多6个人即可建立联系

互联网社交网络的兴起将每个人联系到一起

在社会中有20%擅长交往的人, 建立了80%的连接

区别于随机网络,保证了六度分隔的成立引出了无尺度网络的研究

二、术语表

顶点 Vertex(也“节点node”)

是图的基本组成部分,定点具有名称标识key,也可以携带数据项payload。

边 Edge(也称“弧Arc”)

作为2个顶点之间关系的表示,边连接两个顶点;边可以是有向的或者无向的,相应的图称做“有向图”和“无向图”。

权重 Weight

为了表达从一个顶点到另一个顶点的“代价”,可以给边赋权;例如公交网络中两个站点之间的“距离”、“通行时间”和“票价”都可以作为权重。

路径Path

图中的路径,是由边依次链接起来的顶点序列;无权路径的长度为边的数量;带权路径的长度为所有边权重的和;如下图的一条路径(v3,v4,v0,v1)

圈Cycle

圈是首尾顶点相同的路径,如下图中(V5,V2,V3,V5)

如果有向图中不存在任何圈,则称为“有向无圈图 directed acyclic graph:DAG”

如果一个问题能表示成DAG,就可以用图算法很好地解决。

三、图抽象数据类型:ADT Graph

3.1 定义

Graph() 创建一个空图

addVertex(vert) 将顶点vert加入图中

addEdge(fromVert, toVert) 添加有向边

addEdge(fromVert, toVert, weight) 添加带权的有向边

getVertex(vKey) 查找名称为vKey的顶点

getVertices() 返回图中所有顶点列表

in 按照vert in graph的语句形式,返回顶点是否存在图中True/False

3.2 ADT Graph的实现方法

两种方法各有优劣,需要在不同应用中加以选择

邻接矩阵adjacency matrix

邻接表adjacency list

3.2.1 邻接矩阵Adjacency Matrix

矩阵的每行和每列都代表图中的顶点,如果两个顶点之间有边相连,设定行列值

无权边则将矩阵分量标注为1,或者0

带权边则将权重保存为矩阵分量值

例如下面的带权图:

邻接矩阵顶实现法的优点是简单,可以很容易得到顶点是如何相连
但如果图中的边数很少则效率低下,成为“稀疏sparse”矩阵,而大多数问题所对应的图都是稀疏的,边远远少于|V|2这个量级,从而出现邻接列表。

3.2.2 邻接列表Adjacency List

邻接列表可以成为稀疏图的更高效实现方案

维护一个包含所有顶点的主列表(master list)。主列表中的每个顶点,再关联一个与自身由边链接的所有顶点的列表。

邻接列表法的寻出空间紧凑高效

很容易获得顶点所连接的所有顶点以及边的信息

例如上面的图转为邻接列表,与V0有关的有V1和V5,权重分别是5和2:

四、ADT Graph的实现:实例

4.1 Vertex类

下面展示了 Vertex 类的代码,包含了顶点信息, 以及顶点连接边信息

class Vertex:
    def __init__(self,key):
        self.id = key
        self.connectedTo = {}
	#从这个顶点添加一个连接到另一个
    def addNeighbor(self,nbr,weight=0):  #nbr是顶点对象的key
        self.connectedTo[nbr] = weight
	#顶点数据字符串化,方便打印
    def __str__(self):
        return str(self.id) + ' connectedTo: ' + str([x.id for x in self.connectedTo])
	#返回邻接表中的所有顶点
    def getConnections(self):
        return self.connectedTo.keys()
	#返回key
    def getId(self):
        return self.id
	#返回顶点边的权重。
    def getWeight(self,nbr):
        return self.connectedTo[nbr]

4.2 Graph 类

下面展示了 Graph 类的代码,包含将顶点名称映射到顶点对象的字典。Graph 还提供了将顶点添加到图并将一个顶点连接到另一个顶点的方法。getVertices方法返回图中所有顶点的名称。此外,我们实现了__iter__ 方法,以便轻松地遍历特定图中的所有顶点对象。 这两种方法允许通过名称或对象本身在图形中的顶点上进行迭代。

class Graph:
    def __init__(self):
        self.vertList = {}
        self.numVertices = 0
	#新加顶点
    def addVertex(self,key):
        self.numVertices = self.numVertices + 1
        newVertex = Vertex(key)
        self.vertList[key] = newVertex
        return newVertex
	#通过key查找顶点
    def getVertex(self,n):
        if n in self.vertList:
            return self.vertList[n]
        else:
            return None

    def __contains__(self,n):
        return n in self.vertList
   
    def addEdge(self,f,t,cost=0):
        if f not in self.vertList: #不存在的顶点先添加
            nv = self.addVertex(f)
        if t not in self.vertList:
            nv = self.addVertex(t)
        self.vertList[f].addNeighbor(self.vertList[t], cost)

    def getVertices(self):
        return self.vertList.keys()

    def __iter__(self):
        return iter(self.vertList.values())


g = Graph()
for i in range(6):
    g.addVertex(i)
g.addEdge(0,1,5)
g.addEdge(0,5,2)
g.addEdge(1,2,4)
g.addEdge(2,3,9)
g.addEdge(3,4,7)
g.addEdge(3,5,3)
g.addEdge(4,0,1)
g.addEdge(5,4,8)
g.addEdge(5,2,1)
for v in g:  #遍历输出
    for w in v.getConnections():
        print("( %s , %s )" % (v.getId(), w.getId()))

Out: 
{0: <__main__.Vertex at 0x2459cdf65f8>,
 1: <__main__.Vertex at 0x2459cdf6630>,
 2: <__main__.Vertex at 0x2459cdf6668>,
 3: <__main__.Vertex at 0x2459cdf66a0>,
 4: <__main__.Vertex at 0x2459cdf66d8>,
 5: <__main__.Vertex at 0x2459cdf6710>}
( 0 , 1 )
( 0 , 5 )
( 1 , 2 )
( 2 , 3 )
( 3 , 4 )
( 3 , 5 )
( 4 , 0 )
( 5 , 4 )
( 5 , 2 )

五、图的应用

5.1 词梯问题

由 “ 爱 丽 丝 漫 游 奇 境 ” 的 作 者 LewisCarroll在1878年所发明的单词游戏。

从一个单词演变到另一个单词, 其中的过程可以经过多个中间单词,要求是相邻两个单词之间差异只能是1个字母,如FOOL变SAGE:

FOOL >> POOL >> POLL >> POLE >> PALE>> SALE >> SAGE

我们的目标是找到最短的单词变换序列,采用图来解决这个问题的步骤如下:

将可能的单词之间的演变关系表达为图

采用“广度优先搜索 BFS”,来搜寻从开始单词到结束单词之间的所有有效路径

选择其中最快到达目标单词的路径

5.1.1 构建单词关系图

首先是如何将大量的单词集放到图中:将单词作为顶点的标识Key,如果两个单词之间仅相差1个字母,就在它们之间设一条边

下图是从FOOL到SAGE的词梯解, 所用的图是无向图, 边没有权重

单词关系图可以通过不同的算法来构建(以4个字母的单词表为例)

1.首先是将所有单词作为顶点加入图中,再设法建立顶点之间的边

2.建立边的最直接算法, 是对每个顶点(单词) , 与其它所有单词进行比较, 如果相差仅1个字母, 则建立一条边时间复杂度是O(n^2),对于所有4个字母的5110个单词,需要超过2600万次比较

3.改进的算法是创建大量的桶, 每个桶可以存放若干单词

4.桶标记是去掉1个字母,通配符“_”占空的单词

5.所有单词就位后,再在同一个桶的单词之间建立边即可

5.1.2 采用字典建立桶

def buildGraph(wordfile):
    d = {}
    g = Graph()
    wfile = open(wordfile,'r')
    for line in wfile:
        word = line[:-1]
        for i in range(len(word)):
            bucket = word[:i] + '_' + word[i+1:]
            if bucket in d:
                d[bucket].append(word)
            else:
                d[bucket] = [word]
        #同一个桶的单词建立边
        for bucket in d.keys():
            for word1 in d[bucket]:
                for word2 in d[bucket]:
                    if word1 != word2:
                        g.addEdge(word1, word2)
        return g

5.2 拓扑排序(Topological Sort)

很多问题都可转化为图, 利用图算法解决,例如早餐吃薄煎饼的过程,以动作为顶点,以先后次序为有向边。(有先后次序和依赖关系)

图得到工作次序排列的算法,称为“拓扑排序”

拓扑排序处理一个有向无圈图(DAG), 输出顶点的线性序列:使得两个顶点v,w,如果G中有(v,w)边,在线性序列中v就出现在w之前。

拓扑排序广泛应用在依赖事件的排期上,还可以用在项目管理、 数据库查询优化和矩阵乘法的次序优化上。拓扑排序可以采用DFS很好地实现:

将工作流程建立为图,工作项是节点,依赖关系是有向边

工作流程图一定是个DAG图,否则有循环依赖

对DAG图调用DFS算法,以得到每个顶点的“结束时间”,按照每个顶点的“结束时间”从大到小排序

输出这个次序下的顶点列表

5.3 强连通分支

我们关注一下互联网相关的非常巨大图:

  • 由主机通过网线(或无线)连接而形成的图;

  • 以及由网页通过超链接连接而形成的图。

先看网页形成的图
网页(URI作为id)为顶点,网页内包含的超链接作为边,可以转换为一个有向图。
图中包含了许多路德学院其它系的网站,包含了一些爱荷华其它大学学院的网站,还包含了一些人文学院的网站

我们可以猜想, Web的底层结构可能存在某些同类网站的聚集

在图中发现高度聚集节点群的算法, 即寻找“强连通分支Strongly Connected Components”算法

强连通分支, 定义为图G的一个子集C:C中的任意两个顶点v,w之间都有路径来回,即(v,w)(w,v)都是C的路径,而且C是具有这样性质的最大子集

下图是具有3个强连通分支的9顶点有向图,一旦找到强连通分支, 可以据此对图的顶点进行分类, 并对图进行化简(9个顶点变为3个顶点,相当于聚类)

5.3.1 强连通分支算法:转置概念

在用深度优先搜索来发现强连通分支之前, 先熟悉一个概念: Transposition转置

一个有向图G的转置GT,定义为将图G的所有边的顶点交换次序,如将(v,w)转换为(w,v)。可以观察到图和转置图在强连通分支的数量和划分上,是相同的

下图互为转置,即

5.3.2 强连通分支算法: Kosaraju算法思路

5.3.3 Kosaraju算法实例

第一趟DFS

转置后第二趟DFS, 取结束时间最大的第一个开始

结果形成三个强连通分支:

Kosaraju算法最好理解,但是效率最差,因此另外的常用强连通分支算法有:

  • Tarjan算法

  • Gabow算法,对Tarjan的改进

5.4 最短路径问题

5.4.1 介绍

当我们通过网络浏览网页、 发送电子邮件、 QQ消息传输的时候, 数据会在联网设备之间流动。

所以我们可以将互联网路由器体系表示为

一个带权边的图:

路由器作为顶点,路由器之间网络连接作为边

权重可以包括网络连接的速度、网络负载程度、分时段优先级等影响因素

作为一个抽象,我们把所有影响因素合成为单一的权重

解决信息在路由器网络中选择传播速度最快路径的问题, 就转变为在带权图上最短路径的问题。
与广度优先搜索BFS算法解决的词梯问题相似, 只是在边上增加了权重

5.4.2 Dijkstra算法

解决带权最短路径问题的经典算法是以发明者命名的“Dijkstra算法”( /ˈdɛɪkstra)

这是一个迭代算法, 得出从一个顶点到其余所有顶点的最短路径, 很接近于广度优先搜索算法BFS的结果

具体实现上, 在顶点Vertex类中的成员dist用于记录从开始顶点到本顶点的最短带权路径长度(权重之和) , 算法对图中的每个顶点迭代一次

步骤:

顶点的访问次序由一个优先队列来控制,队列中作为优先级的是顶点的dist属性。

最初, 只有开始顶点dist设为0, 而其他所有顶点dist设为sys.maxsize(最大整数) , 全部加入优先队列;

随着队列中每个最低dist顶点率先出队;

并计算它与邻接顶点的权重, 会引起其它顶点dist的减小和修改, 引起堆重排;

并据更新后的dist优先级再依次出队。

以下图为例:

设u为开始顶点,计算与u相连的其他顶点的权重,并将u出队。

更新v,x的权重,将较小的x(d=1)出队

计算与x相连的v,w,y的权重,v:1+2=3 > 2,因此不更新权重,其余的更新。将最小的v出队

同时y(d=2)也出队,计算与之相连的w,z,并更新w,z

将w,z出队

form pythonds.graphs import Graph,PriorityQueue,Vertex
def dijkstra(aGraph, start):
    pq = PriorityQueue()
    start.setDistance(0)#开始点的距离设为0
    pq.buildHeap([(v.getDistance(), v) for v in aGraph]) #对所有顶点建堆,形成优先队列
    while not pq.isEmpty():
        currentVert = pq.delMin() #优先队列出队
        for nextVert in currentVert.getConnections():
            newDist = currentVert.getDistance() + currentVert.getWeight(nextVert)
            if newDist < nextVert.getDistance():#修改出队顶点所邻接顶点的dist,并逐个顶点的dist,并逐个
                nextVert.setDistance(newDist)
                nextVert.setPred(currentVert)
                pq.decreaseKey(nextVert, newDist)

需要注意的是, Dijkstra算法只能处理大于0的权重,如果图中出现负数权重,则算法会陷入无限循环。

虽然Dijkstra算法完美解决了带权图的最短路径问题, 但实际上Internet的路由器中采用的是其它算法,其中最重要的原因是, Dijkstra算法需要具备整个图的数据, 但对于Internet的路由器来说, 显然无法将整个Internet所有路由器及其连接信息保存在本地,路由器的选径算法(或“路由算法”) 对于互联网极其重要, 有兴趣可以进一步参考“距离向量路由算法“

5.4.3 Dijkstra算法分析

首先, 将所有顶点加入优先队列并建堆,时间复杂度为O(|V|)

其次, 每个顶点仅出队1次, 每次delMin 花费O(log|V|), 一共就是O(|V|log|V|)

另外, 每个边关联到的顶点会做一次decreaseKey操作(O(log|V|)), 一共是O(|E|log|V|)

上 面 三 个 加 在 一 起 , 数 量 级 就 是O ( ( ∣ V ∣ + ∣ E ∣ ) l o g ∣ V ∣ ) O((|V|+|E|)log|V|)O((∣V∣+∣E∣)log∣V∣)

5.5 最小生成树

本算法涉及到在互联网中网游设计者和网络收音机所面临的问题:信息广播问题

信息广播问题最简单的解法是:由广播源维护一个收听者的列表, 将每条消息向每个收听者发送一次

信息广播问题的暴力解法, 是将每条消息在路由器间散布出去,如果没有任何限制, 这个方法将造成网络洪水灾难。

洪水解法一般会给每条消息附加一个生命值(TTL:Time To Live) , 初始设置为从消息源到最远的收听者的距离;每个路由器收到一条消息, 如果其TTL值大于0, 则将TTL减少1, 再转发出去,TTL的设置防止了灾难发生, 但这种洪水解法显然比前述的单播方法所产生的流量还要大。

信息广播问题的最优解法, 依赖于路由器关系图上选取具有最小权重的生成树(minimum weight spanning tree)

生成树:拥有图中所有的顶点和最少数量的边,以保持连通的子图。

图G(V,E)的最小生成树T, 定义为包含所有顶点V,以及E的无圈子集,并且边权重之和最小。

图为一个最小生成树,这样信息广播就只需要从A开始, 沿着树的路径层次向下传播就可以达到每个路由器只需要处理1次消息,同时总费用最小

5.5.1 Prim算法

解决最小生成树问题的Prim算法, 属于“贪心算法”, 即每步都沿着最小权重的边向前搜索。

构造最小生成树的思路很简单, 如果T还不是生成树, 则反复做:

找到一条最小权重的可以安全添加的边,将边添加到树T

“可以安全添加”的边, 定义为一端顶点在树中, 另一端不在树中的边, 以便保持树的无圈特性

from pq import PriorityQueue, Graph, Vertex
# 最小生成树prim算法
# G - 无向赋权图
# start - 开始节点
# 返回从开始节点创建最小生成树
def prim(G, start):
    pq = PriorityQueue()  # 创建优先队列
    start.setDistance(0)  # 起点最小权重代价设置为0,其它节点最小权重代价默认maxsize
    # 将节点排入优先队列,start在最前面
    pq.buildHeap([(v.getDistance(), v) for v in G])
    while not pq.isEmpty():
        # 取距离*已有树*最小权重代价的节点出队列,作为当前节点
        # 当前节点已解出最小生成树的前驱pred和对应最小权重代价dist
        currentVert = pq.delMin()
        # 遍历节点的所有邻接节点
        for nextVert in currentVert.getConnections():
            # 从当前节点出发,逐个检验到邻接节点的权重
            newCost = currentVert.getWeight(nextVert)
            # 如果邻接节点是"安全边",并且小于邻接节点原有最小权重代价dist,就更新邻接节点
            if nextVert in pq and newCost < nextVert.getDistance():
                # 更新最小权重代价dist
                nextVert.setPred(currentVert)
                # 更新返回路径
                nextVert.setDistance(newCost)
                # 更新优先队列
                pq.decreaseKey(nextVert, newCost)


G = Graph()
ndedge = [('A', 'B', 2), ('A', 'C', 3), ('B', 'C', 1),
          ('B', 'D', 1), ('B', 'E', 4), ('C', 'F', 5),
          ('D', 'E', 1), ('E', 'F', 1), ('F', 'G', 1)]
for nd in ndedge:
    G.addEdge(nd[0], nd[1], nd[2])
    G.addEdge(nd[1], nd[0], nd[2])
start = G.getVertex('A')
prim(G, start)
print(G)

G = Graph()
ndedge = [('v1', 'v2', 6), ('v1', 'v3', 1), ('v1', 'v4', 5),
          ('v2', 'v3', 5), ('v3', 'v4', 5), ('v2', 'v5', 3),
          ('v3', 'v5', 6), ('v3', 'v6', 4), ('v4', 'v6', 2),
          ('v5', 'v6', 6)]
for nd in ndedge:
    G.addEdge(nd[0], nd[1], nd[2])
    G.addEdge(nd[1], nd[0], nd[2])
start = G.getVertex('v1')
prim(G, start)
print(G)

pq.py

import sys


class Graph:
    def __init__(self):
        self.vertices = {}
        self.numVertices = 0

    def addVertex(self, key):
        self.numVertices = self.numVertices + 1
        newVertex = Vertex(key)
        self.vertices[key] = newVertex
        return newVertex

    def getVertex(self, n):
        if n in self.vertices:
            return self.vertices[n]
        else:
            return None

    def __contains__(self, n):
        return n in self.vertices

    def addEdge(self, f, t, cost=0):
        if f not in self.vertices:
            nv = self.addVertex(f)
        if t not in self.vertices:
            nv = self.addVertex(t)
        self.vertices[f].addNeighbor(self.vertices[t], cost)

    def getVertices(self):
        return list(self.vertices.keys())

    def __iter__(self):
        return iter(self.vertices.values())

    def __str__(self):
        s = ""
        for v in self:
            s += f"[{v.id},{v.dist},{v.pred.id if v.pred else None}] "
        return s


class Vertex:
    def __init__(self, num):
        self.id = num
        self.connectedTo = {}
        self.color = 'white'
        self.dist = sys.maxsize
        self.pred = None
        self.disc = 0
        self.fin = 0

    # def __lt__(self,o):
    #     return self.id < o.id

    def addNeighbor(self, nbr, weight=0):
        self.connectedTo[nbr] = weight

    def setColor(self, color):
        self.color = color

    def setDistance(self, d):
        self.dist = d

    def setPred(self, p):
        self.pred = p

    def setDiscovery(self, dtime):
        self.disc = dtime

    def setFinish(self, ftime):
        self.fin = ftime

    def getFinish(self):
        return self.fin

    def getDiscovery(self):
        return self.disc

    def getPred(self):
        return self.pred

    def getDistance(self):
        return self.dist

    def getColor(self):
        return self.color

    def getConnections(self):
        return self.connectedTo.keys()

    def getWeight(self, nbr):
        return self.connectedTo[nbr]

    def __str__(self):
        return str(self.id) + ":color " + self.color + ":disc " + str(self.disc) + ":fin " + str(
            self.fin) + ":dist " + str(self.dist) + ":pred \n\t[" + str(self.pred) + "]\n"

    def __repr__(self):
        return str(self.id)

    def getId(self):
        return self.id


class PriorityQueue:
    def __init__(self):
        self.heapArray = [(0, 0)]
        self.currentSize = 0

    def buildHeap(self, alist):
        self.currentSize = len(alist)
        self.heapArray = [(0, 0)]
        for i in alist:
            self.heapArray.append(i)
        i = len(alist) // 2
        while (i > 0):
            self.percDown(i)
            i = i - 1

    def percDown(self, i):
        while (i * 2) <= self.currentSize:
            mc = self.minChild(i)
            if self.heapArray[i][0] > self.heapArray[mc][0]:
                tmp = self.heapArray[i]
                self.heapArray[i] = self.heapArray[mc]
                self.heapArray[mc] = tmp
            i = mc

    def minChild(self, i):
        if i * 2 > self.currentSize:
            return -1
        else:
            if i * 2 + 1 > self.currentSize:
                return i * 2
            else:
                if self.heapArray[i * 2][0] < self.heapArray[i * 2 + 1][0]:
                    return i * 2
                else:
                    return i * 2 + 1

    def percUp(self, i):
        while i // 2 > 0:
            if self.heapArray[i][0] < self.heapArray[i // 2][0]:
                tmp = self.heapArray[i // 2]
                self.heapArray[i // 2] = self.heapArray[i]
                self.heapArray[i] = tmp
            i = i // 2

    def add(self, k):
        self.heapArray.append(k)
        self.currentSize = self.currentSize + 1
        self.percUp(self.currentSize)

    def delMin(self):
        retval = self.heapArray[1][1]
        self.heapArray[1] = self.heapArray[self.currentSize]
        self.currentSize = self.currentSize - 1
        self.heapArray.pop()
        self.percDown(1)
        return retval

    def isEmpty(self):
        if self.currentSize == 0:
            return True
        else:
            return False

    def decreaseKey(self, val, amt):
        # this is a little wierd, but we need to find the heap thing to decrease by
        # looking at its value
        done = False
        i = 1
        myKey = 0
        while not done and i <= self.currentSize:
            if self.heapArray[i][1] == val:
                done = True
                myKey = i
            else:
                i = i + 1
        if myKey > 0:
            self.heapArray[myKey] = (amt, self.heapArray[myKey][1])
            self.percUp(myKey)

    def __contains__(self, vtx):
        for pair in self.heapArray:
            if pair[1] == vtx:
                return True
        return False

    def __str__(self):
        return str(self.heapArray)