数据结构与算法数据结构ios开发

数据结构与算法(一):数据结构

2018-08-26  本文已影响2221人  一剑孤城

大部分内容来自于《大话数据结构》,代码全部使用Swift实现。至于为什么抽风写这个?😊你懂的。

一. 数据结构一些重要定义

理解数据结构之前,先来了解一些重要的概念以及这些概念之间的关系。

(1)数据

数据:可以被计算机理解和处理的描述客观事物的符号集。

几个特征:

比如:整型,字符串等等。都可以满足上面上个条件。
那么?图像,声音算不算数据。答案肯定算,图像,声音这种客观事物,在计算机中转化成二进制来储存,所以,满足上面的条件。

(2)数据的组成结构

DataStructure.png

数据元素:数据里面具有一定意义的基本单位。

把人类比做数据,那么,数据元素就是人的单独个体。

数据项:数据项是数据不可分割的最小单位。

如果数据元素比做人,那么,数据项可以是组成人的各种器官,眼,鼻,耳,嘴等等,也可以是年龄,性别,姓名等等。

数据对象:性质相同的数据元素的集合,是数据的子集。

性质相同,那就好理解了,比如:兴趣相同的人组成的群体或者90后群体等等。

(3)数据结构

了解到数据其实就是符号集,那么?数据结构是个什么东东?顾名思义,数据结构就是数据的组织形式。类比建筑工程里面的设计结构。

数据结构:数据结构就是数据之间的组织形式。

打个比方:假如你有1000个苹果,你会怎么储存?

一种方案:找一个足够大的储存室,把苹果放在一排
一种方案:100个苹果放一排,总共放10排
一种方案:100个苹果放在底部,上面再叠10层
...

上面N中方案都是处理这个1000个苹果的思路,怎么存放涉及到整个架构的设计,好比数据结构就是解决数据之间的组织结构问题的科学。

数据结构分类

DataStructure2.png

集合结构:集合结构中的数据元素除了同属一个集合外,没有其他关系。
线性结构:线性结构中的数据元素之间是一对一的关系。
树形结构:树形结构中的数据元素之间是一对多的关系。
图形结构:图形结构中的数据元素之间是多对多的关系。

顺序储存结构:把数据元素储存在连续的存储单元。
链式储存结构:把数据元素储存在任意的存储单元(可以是连续或者不连续)。

(4)数据类型 VS 抽象数据类型 VS 数据结构

数据类型:是指一组性质相同的值的集合以及定义在此集合伤的一些操作的总称。
C语言数据类型分类:

抽象数据类型:是指数据类型数学抽象模型以及定义在该模型上的一组操作(抽象数据类型不仅仅指已定义并实现的数据类型,还可以是自定义的数据类型。)。

抽象数据类型的格式:
ADT 抽象数据类型名称
Data 
        数据元素之间的逻辑关系的定义
Operation
        操作1
        操作2
            ...
        操作N
EndADT

数据类型 VS 抽象数据类型

数据类型是抽象数据类型的逻辑和物理上的具体实现,抽象数据类型则负责定义数据类型上具有普遍性的逻辑关系以及相关的一组操作。比如说:整型,抽象数据类型的整型是指整型具有的普遍特性,比如拥有加减乘除的操作,但是并不涉及到整型在每种语言的具体实现或者不同的操作系统储存不一样等等;涉及到物理上的具体实现则表示为数据类型。类比类和接口

抽象数据类型 VS 数据结构

抽象数据类型强调的是数据的逻辑性,而数据结构则强调的是数据元素之间的组织形式,其中包括逻辑结构和物理结构。两者并没有所谓的包含关系,但是有交集,可以理解为数据结构已经站在更高的层次统筹数据的合理组织像是,而抽象数据类型还停留在数据层面,比数据类型更高一层,更具有普遍性的抽象。

类比:
数据类型 -> 类
抽象数据类型 -> 接口
数据结构 -> 一个装着类的实例的数组

二. 线性表

线性表:零个或者多个数据元素的有限序列。

性质:

举个例子:
白羊 -> 金牛 -> 双子 -> 巨蟹 -> 狮子 -> 处女 -> 天秤 -> 射手 -> 摩羯 -> 水平 -> 双鱼

线性表的抽象数据类型:
ADT 线性表(List)
Data
      线性表的数据对象集合为{a1, a2, ......, an},每一个元素的类型都是DataType。其中,除第一个元素a1外,每一个元素有且仅有一个直接前驱元素,除了最后一个元素an外,每一个元素有且仅有一个直接后续元素。数据元素之间的关系是一对一的关系。
Operation
       count:线性表元素个数。
       first:头指针。
       last:尾指针。
       isEmpty():若线性表为空,返回true,否则返回false。
       remove():将线性表清空
       node(i):将线性表中的第i个位置的元素返回。
       insert(data,i):在线性表中的第i个位置插入新数据data。
EndADT

线性表根据在计算机的储存方式可以分为两种:

顺序线性表

顺序线性表:使用一段连续的地址存储单元放置线性表的数据元素。

举个例子:数组。

顺序线性表的优缺点:
优点:
- 可以快速获取下标的数据元素,时间复杂度为O(1)
- 逻辑关系是一对一的关系,连续存储单元足以储存,不需要增加额外的存储空间

缺点:
- 插入和删除操作需要移动大量的元素,时间复杂度为O(n)
- 线性表的存储空间大小难以确定,并且不好扩展
- 造成存储空间碎片

链式线性表

链式线性表:线性表的数据元素可以存储在随意的存储单元,每一个节点不仅仅包括数据元素还有一个指向下一个节点的指针(基本的单链表)。

链式(单链表)和顺序线性表优缺点对比:

存储分配方式:
- 顺序 -> 一段地址连续的存储空间
- 链式 -> 任意地址存储空间

时间性能:
- 查找
  顺序 -> O(1)
  链式 -> O(n)
- 插入和删除
  顺序 -> O(n)
  链式 -> 寻找相应的节点,时间复杂度为O(n),然后,插入和删除为O(1)

空间性能:
- 顺序 -> 需要提前分配存储空间,分配大了,浪费空间,分配小了,容易发生上溢
- 链式 -> 不需要提前分配空间,只要有存储空间分配就行,数据元素个数只受可分配存储空间大小的限制

总结:
(1)若线性表需要频繁查找,很少进行插入和删除操作时,使用顺序存储结构;反之,使用链式存储结构。
(2)如果提前知道线性表需要的存储空间,可以使用顺序结构;如果不知道线性表中的数据元素变化有多大,即不确定需要多大的存储空间,则使用链式存储结构。

链式线性表的基本分类:

下面具体讲解双链表的Swift实现,其他的链表实现可以参考《大话数据结构》或者自行Googel/Baidu。

双向链表:在单链表的基础上,每个节点加一个指向上一个节点的指针。

节点定义:

public class LinkedListNode<T> {
    var data: T  //Data could not be nil.
    var previous: LinkedListNode?  //The pointer to previous node.
    var next: LinkedListNode?  //The pointer to next node.
    init(_ data: T) {
        self.data = data
    }
}

双向链表:

public enum ErrorStatus {
    case Error(message: String)
    case OK
}

public class DoubleLinkedList<T> {
    public typealias Node = LinkedListNode<T>
    
    private var head: Node?  //Head node of link list.
    
    public var isEmpty: Bool {  //If link list has no data, return true.
        return head == nil
    }
    
    public var first: Node? {  //Get first node is the head of link list.
        return head
    }
    
    public var last: Node? {  //Last node of link list.
        ...
        return node
    }
    
    public var count: Int {  //Retrun link list's nodes count.
        ...
        return count
    }
    
    public func node(atIndex index: Int) -> Node? {  //Get node with index
        ...
        return node
    }
    
    public func appendData(data: T) {  //Append data to link list tail
        ...
    }
    
    public func insert(data: T, atIndex index: Int) -> ErrorStatus {  //Insert data at index
        guard index >= 0, index <= count else {
            return ErrorStatus.Error(message: "Index is out of range!")
        }
        let newNode = Node(data)
        if index == 0 {
            if let node = first {
                head = newNode
                newNode.next = node
                node.previous = newNode
            } else {
                head = newNode
            }
        } else {
            let node = self.node(atIndex: index-1)
            let nextNode = self.node(atIndex: index)
            node?.next = newNode
            newNode.previous = node
            newNode.next = nextNode
            nextNode?.previous = newNode
        }
        return ErrorStatus.OK
    }
    
    public func remove(atIndex index: Int) -> (T?, ErrorStatus) {  //Remove node at index
        guard !isEmpty else {
            return (nil, ErrorStatus.Error(message: "Link list is Empty!"))
        }
        
        guard index >= 0, index < count else {
            return (nil, ErrorStatus.Error(message: "Index is out of range!"))
        }
        
        let node = self.node(atIndex: index)
        let nextNode = self.node(atIndex: index+1)
        if index == 0 {
            head = nextNode
        } else {
            let beforeNode = self.node(atIndex: index-1)
            beforeNode?.next = nextNode
            nextNode?.previous = beforeNode
        }
        return (node?.data, ErrorStatus.OK)
    }    
}

⚠️注意:
这里的head指向第一个有数据的节点,有的线性表会生成一个头节点,该节点不存储任何数据或者只存储该链表的长度,该节点指向第一个有数据的节点。这样做的好处就是,第一个节点的删除和插入操作和其他节点保持一致。

下面主要解释一下,插入和删除操作:

public func insert(data: T, atIndex index: Int) -> ErrorStatus {  //Insert data at index
    guard index >= 0, index <= count else {
        return ErrorStatus.Error(message: "Index is out of range!")
    }
    let newNode = Node(data)
    if index == 0 {
        if let node = first {
            head = newNode
            newNode.next = node
            node.previous = newNode
        } else {
            head = newNode
        }
    } else {
        let node = self.node(atIndex: index-1)
        let nextNode = self.node(atIndex: index)
        node?.next = newNode
        newNode.next = nextNode
        newNode.previous = node
        nextNode?.previous = newNode
    }
    return ErrorStatus.OK
}

1.先判断需要插入数据的index是否在[0, count]的范围之内,注意这里是方括号,也就是包含边界,因为线性表最前面和最后面都可以插入新的数据。
2.生成新节点。
3.因为这里的双向链表没有采取头节点的方式实现,所以,插入第一个节点和其他节点有点不一样,需要做一些判断。
4.如果是插入第一个节点,则判断如果该链表为空,则直接设置head=newNode;如果该链表不为空,则将一个节点赋值给node,然后将newNode赋值给head,接着将node赋值给newNode.next,最后设置node.previous=newNode。
5.如果不是插入一个节点,则先获取下标为index-1的节点node,然后获取下标为index的节点nextNode。设置node.next=newNode,然后newNode.next=nextNode,连成一条指向下一个数据元素的链,最后设置newNode.previous=node和nextNode.previous=newNode连上指向上一个数据元素的链,自此,先的数据插入成功。

public func remove(atIndex index: Int) -> (T?, ErrorStatus) {  //Remove node at index
    guard !isEmpty else {
        return (nil, ErrorStatus.Error(message: "Link list is Empty!"))
    }
    
    guard index >= 0, index < count else {
        return (nil, ErrorStatus.Error(message: "Index is out of range!"))
    }
    
    let node = self.node(atIndex: index)
    let nextNode = self.node(atIndex: index+1)
    if index == 0 {
        head = nextNode
    } else {
        let beforeNode = self.node(atIndex: index-1)
        beforeNode?.next = nextNode
        nextNode?.previous = beforeNode
    }
    return (node?.data, ErrorStatus.OK)
}

1.先判断是否是空链,如果是则返回,否则再判断需要删除数据的小表是否在合理范围内,如果不是则返回。
2.判断index是否等于0,如果是,则直接将head=secondNode。
3.获取beforeNode和nextNode,然后将beforeNode.next=nextNode,nextNode,previous=beforeNode,自此,下标为index的节点,没有任何对象指向它,在当前函数域外就外被系统回收掉。

三. 栈与队列

栈:限定在表尾进行插入和删除的线性表。

栈是一种LIFO(Last In First Out)的线性表,也就是数据元素遵循后进先出的原则。

栈的抽象数据类型:

ADT 栈(Stack)
Data
      具有线性表一样的性质。
Operation
      top:获取栈顶元素
      count:栈的长度
      isEmpty:栈内元素是否为空
      push(data):元素进栈
      pop():元素出栈
      removeAll():清空栈内元素
EndADT

按栈的存储结构分类:

共享栈:
两个顺序栈共享存储空间,栈1的栈顶在下标0处,栈2的栈顶在下标n-1处。

栈的顺序结构和链式结构区别:
1.顺序结构需要预先估算存储空间大小,适合数据元素变化不大的情况
2.链式结构不需要预先估算存储空间,适合数据元素变化较大的情况

栈的链式结构实现:

public struct Stack<T> {
    private var stackData: [T] = []
    
    public var count: Int {
        return stackData.count
    }
    
    public var top: T? {
        if isEmpty {
            return nil
        } else {
            return stackData.last
        }
    }
    
    public var isEmpty: Bool {
        return stackData.isEmpty
    }
    
    public mutating func push(data: T) {
        stackData.append(data)
    }
    
    public mutating func pop() -> T? {
        if isEmpty {
            return nil
        } else {
            return stackData.removeLast()
        }
    }
    
    public mutating func removeAll() {
        stackData.removeAll()
    }
    
    public func printAllData() {
        for item in stackData {
            print(item)
        }
    }
}

上面的代码写的非常清楚,一目了然,这里就不费口舌了。

栈较之线性表有什么特别作用?😊

栈和线性表的不同之处在于,栈只有进栈和出栈操作,并且遵循后进先出的规则,也就是说数据元素顺序进栈,逆序出栈。嘿嘿,栈可以实现回退操作,递归和四则运算等等。

所经过的月数 1 2 3 4 5 6 7 8
兔子对数 1 1 2 3 5 8 13 21

以月数为自变量,兔子的对数为因变量,则它们之间的关系为:

F(0) = 0, n=0
F(1) = 1, n=1
F(n) = F(n-1) + F(n-2), n>1

下面使用常规的方法解决这个问题:

func rabbitNumbers() {
    //After 40 months
    var r0 = 0
    print("rabbit: \(r0)")
    var r1 = 1
    print("rabbit: \(r1)")
    var r: Int = 0
    for _ in 2..<40 {
        r = r0 + r1
        print("rabbit: \(r)")
        r0 = r1
        r1 = r
    }
}

再者使用递归解决这个问题:

func rabbitNumbers(months n: Int) -> Int {
    if n < 2 {
        return n
    }
    return rabbitNumbers(months: n-1)+rabbitNumbers(months: n-2)
}

for i in 0..<40 {
    let rabbits = rabbitNumbers(months: i)
    print("rabbit: \(rabbits)")
}

比较一下前面两个的差异:

常规方法:
    使用一个迭代的方式,计算出结果。不需要反复调用函数,浪费内存。但是,相对于递归来说,没有那么简洁。
递归:
    递归就需要不断调用函数,建立函数副本,消耗大量的内存。但是,相对于常规方法,代码简洁易懂。

为什么说递归是栈的应用呢?
递归在执行的时候,需要不断的调用自身,直到可以返回确定的值,然后,又从最后面的一层,一层层往回调,直到最上一层,这个两个操作正好对应着栈的压栈和出栈的操作。其实,编译器也是这样干的,在前行阶段,对于每一层递归,函数的局部变量,参数值和返回地址都压进栈;在退回阶段,位于栈顶的局部变量,参数值以及返回地址都被弹出,用于返回调用层次中执行代码的剩余部分,恢复调用状态。

一般手动计算数学题我们用的是中缀表达法(符号在需要运算的数字中间):

9 + (3 - 1) * 3 + 10 / 2

计算机运算需要用到的后缀表达法(去调括号,符号在需要运算的数字后面):

9 3 1 - 3 * + 10 2 / +

所以?怎么得到后缀表达式呢?利用栈:

规则:
1.遇到数字,直接输出
2.遇到左括号进栈
3.遇到右括号执行出栈操作,直到弹出左括号,左括号和右括号不输出
4.遇到其他操作符,则判断其与栈顶操作符的优先级,如果其优先级(<=)栈顶操作符,则栈顶元素依次出栈,该操作符进栈
5.直到最后,将栈中的元素依次输出

enum OperationPriority: String {
    case multi = "*"
    case divide = "/"
    case add = "+"
    case sub = "-"
    
    var intValue: Int {
        switch self {
        case .multi,.divide:
            return 1
        case .add, .sub:
            return 0
        }
    }
}

func suffixExpFromMiddleExp(exps: Array<String>) -> Array<String>{
    var suffixExp: [String] = Array()
    var stack = Stack<String>()
    let predicate = NSPredicate.init(format: "SELF MATCHES %@", "^[0-9]*$")
    for value in exps {
        if predicate.evaluate(with: value) {
            suffixExp.append(value)
        } else if value == "(" {
            stack.push(data: value)
        } else if value == ")" {
            while stack.top != "(" {
                suffixExp.append(stack.pop() ?? "")
            }
            if stack.top == "(" {
                stack.pop()
            }
        } else {
            //value <= top, stack.pop;value > top, stack.push
            let advantage = OperationPriority.init(rawValue: value)?.intValue ?? 0
            var topAdvantage: Int = 0
            while let top = stack.top, top != "(" {
                topAdvantage = OperationPriority.init(rawValue: top)?.intValue ?? 0
                if advantage > topAdvantage {
                    break
                } else {
                    suffixExp.append(top)
                    stack.pop()
                }
            }
            stack.push(data: value)
        }
    }
    while let top = stack.top {
        suffixExp.append(top)
        stack.pop()
    }
    return suffixExp
}

let exp = ["9", "+", "(", "3", "-", "1", ")", "*", "3", "+", "10", "/", "2"]  
//中缀表达式:9+(3-1)*3+10/2
//后缀表达式:9 3 1 - 3 * + 10 2 / +

注:后缀表达式更多获取方法。

得到了后缀表达式,那么开始我们的四则运算了。😊

规则:
1.数字直接进栈
2.遇到符号,将栈顶两个数字出栈,进行运算,运算结果进栈

//在上面的枚举加一个计算加减乘除的函数
enum OperationPriority: String {
    ...
    func result(operator1: String, operator2: String) -> Int {
        switch self {
        case .multi:
            return Int(operator1)!*Int(operator2)!
        case .divide:
            return Int(operator1)!/Int(operator2)!
        case .add:
            return Int(operator1)!+Int(operator2)!
        case .sub:
            return Int(operator1)!-Int(operator2)!
        }
    }
}

func mathOperation(exps: Array<String>) -> Int {
    var result = 0
    var stack = Stack<String>()
    let predicate = NSPredicate.init(format: "SELF MATCHES %@", "^[0-9]*$")
    for value in exps {
        if predicate.evaluate(with: value) {
            stack.push(data: value)
        } else {
            let operator2 = stack.pop()
            let operator1 = stack.pop()
            let temp = (OperationPriority.init(rawValue: value)?.result(operator1: operator1!, operator2: operator2!))!
            stack.push(data: "\(temp)")
        }
    }
    if let resultStr = stack.pop() {
        result = Int(resultStr)!
    }
    return result
}
let exp = ["9", "+", "(", "3", "-", "1", ")", "*", "3", "+", "10", "/", "2"]  
//result=20

是不是很有意思,一个简单的四则运算就这样实现出来了。栈在计算机里面用的挺多的,比如,系统管理的对象就是放进一个全局栈里面的,等不需要的时候,一个一个出栈,释放所占的内存。

队列:只允许在一端进行插入操作,而在另一端进行删除操作的线性表。

队列其实和显示社会排队买东西的队列一个道理,都是一边进,一边出,插队是会被所有人鄙视的😒,慎重。

队列的抽象数据类型:

ADT 队列(Queue)
Data
    具有线性表一样的性质。
Operation
    front:队列第一个数据
    count:队列的长度
    isEmpty():队列是否为空
    enQueue(data):数据进队列
    deQueue():数据出队列
    removeAll():清空队列内的所有数据
EndADT

按线性表的存储结构分类:

顺序队列:就是使用数组来模拟队列,但是数组的插入和删除需要移动数据,比较繁琐。
循环顺序队列:在顺序队列的基础上改造,使队列的队头和队尾可以在数组中循环变化,在数据插入和删除就不需要频繁移动数据了。但是顺序队列,都需要提前申请存储空间,还有溢出的可能。
链式队列:为了解决顺序队列的不足,引用链式队列。不需要提前申请空间,只不过会引入频繁申请和释放的操作。

队列的链式结构实现:

public struct Queue<T> {
    private var queueData: [T] = []
    
    public var front: T? {
        return queueData.first
    }
    
    public var isEmpty: Bool {
        return queueData.isEmpty
    }
    
    public var count: Int {
        return queueData.count
    }
    
    public mutating func enQueue(_ data: T) {
        queueData.append(data)
    }
    
    public mutating func deQueue() -> T? {
        if isEmpty {
            return nil
        } else {
            return queueData.removeFirst()
        }
    }
    
    public mutating func removeAll() {
        queueData.removeAll()
    }
    
    public func printAllData() {
        for value in queueData {
            print(value)
        }
    }
}

队列有什么作用?
在开发过程中,接触最多的应该是全局并发队列。为什么要用队列实现呢?在我看来,在线程的优先级一样的情况下,不应该先申请系统资源的先被满足吗?这和在银行排队取钱是一个道理。当然,VIP就可以无视我们这些在前台排队取钱的渣渣,他们有专门的通道,多么痛的领悟😓。

四. 串

串:是由零个或多个字符组成的有限序列,又叫字符串。

字符串在开发的时候很常用,以Objective-C为例,有可变字符串和不可变字符串,两者的实现数据结构应该有点区别;一个是链式存储结构,一个是顺序存储结构。

字符串的抽象数据类型:

ADT 串(String)
Data
    串中元素仅由一个字符组成,相邻元素具有前驱和后续关系。
Operation
    StringEmpty(S):若串S为空,返回true,否则返回false。
    ...
EndADT

按存储结构分类:

字符串的模式匹配
朴素的模式匹配算法:核心思想,需要匹配的字符串和主串从下标0开始匹配,一旦子串不匹配,则子串又从下标0开始匹配,主串则挪到下标1,不断的循环这个过程,直到主串或者字串当前的下标超过字符串的长度或者匹配成功返回。

let s = "goodgoogle"
let t = "google"

func matchString(s: String, t: String) -> Int {
    var post: Int = -1
    guard !s.isEmpty || !t.isEmpty else {
        return post
    }
    
    var i = 0  //s current index
    var j = 0  //t current index
    
    while i < s.count, j < t.count {
        let sIndex = s.index(s.startIndex, offsetBy: i)
        let tIndex = t.index(t.startIndex, offsetBy: j)
        let sStr = s[sIndex]
        let tStr = t[tIndex]
        if sStr == tStr {
            i = i+1
            j = j+1
        } else {
            i = i-j+1
            j = 0
        }
    }
    if j > t.count-1 {
        post = i-t.count
    }
    return post
}

朴素的模式匹配,虽然,简单明了,但是效率低。所以,强大的KMP模式匹配算法就应运而生了。
强大的KMP算法,这个可以看我之前写的KMP字符串匹配算法,这里就不做详细的介绍了。

五. 树

树:n(n>=0)个结点的有限集。

特点:
- n=0时,称为空树。
- 在任意一颗非空树中:
(1)有且仅有一个根结点
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树。

结点分类:

线性结构:
- 第一个数据元素:无前驱
- 最后一个数据元素:无后续
- 中间元素:一个前驱,一个后续

树结构:
- 根结点:无双亲,唯一
- 叶结点:无孩子,可以多个
- 中间结点:一个双亲,多个孩子

树的抽象数据类型:

 ADT  树(tree)
    树是由一个根结点和若干课子树构成。树中结点具有相同数据类型及层次关系。
Operation
    root:返回根结点。
    nodes:树的结点数
    depth:树的深度
    ...
EndADT

树的存储结构:

以下图为例讲解树三种表示法:


树的三种表示法

双亲表示法:在每个结点中,拥有指向双亲结点的指针(可以扩展子结点或者兄弟结点)。

下标 data parent
0 A -1
1 B 0
2 C 0
3 D 1
4 E 2
5 F 2
6 G 3
7 H 3
8 I 3
9 J 4

孩子表示法:每个结点有多个指针域,其中每个指针指向一棵子树的根结点,叫多重链表表示法;把每个结点排列起来,以单链表做存储结构,叫孩子表示法。

孩子表示法

孩子兄弟表示法:一个结点设置两个指针,分别指向该结点的第一个子结点和该结点的右兄弟。

孩子兄弟表示法

二叉树

二叉树:n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者为由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树组成。

由定义而得出二叉树的特点:

二叉树的基本形态:

特殊二叉树:

二叉树的性质:

性质证明:
-> 性质1
最多结点的二叉树是满二叉树,除了叶结点,所有结点的度都为2,所以,第i层最多有2^(i-1)个结点。

-> 性质2
由性质1可以每层的结点数,则k(k>=1)层二叉树总共节点数为:n = 2^0 + 2^1 + ... 2^(k-1)=1*(1-2^k)/(1-2) = 2^k - 1。

-> 性质3
二叉树的总结点数为:n = n0 + n1 + n2(n0为叶结点,n1为度为1的结点数,n2为度为2的结点数);那么结点数为n的二叉树有多少条连接线呢?很明显每个结点,除了根结点都有指向连接线,所以,结点数为n的二叉树的总连接线为n-1;从一个结点的度来算,度为1的结点会有1条连接线,连接子结点;度为2的结点会有2连接线,连接子结点;根结点没有双亲结点,所以没有连向根结点的连接线,那么有:n-1 = 1*n0 + 2*n2;和第一条方程式联合可得:n0=n2+1。

-> 性质4
设置有n个结点的完全二叉树的深度为k,则:
2^(k-1)-1< n <= 2^k-1
因为k是整数,所以:
2^(k-1)<= n < 2^k
取对数得:
k-1<=log2(n)<k,即:k<=log2(n)+1并且k>log2(n)
所以:k=|log2(n)| + 1

-> 性质5.1
由性质1,2可得:
深度为k的二叉树有:结点2^(k-1) - 1的右孩子为:2^k-1,其左孩子为:2^k-2
左孩子除以2得:2^k-2 = 2^(k-1) - 1等于其双亲结点
右孩子除以2得:2^k-1 = 2^(k-1) - 1 + 1/2比其双亲结点大1/2,向下取整为:2^(k-1) - 1
所以,性质5.1成立。
-> 性质5.2
由性质5.1可知,n(n>1)结点的双亲为|n/2|,也就是说,深度最大,最右边的双亲结点<=n/2,则如果2i>n,说明i只能是叶结点不是双亲结点,所以,没有左孩子。由5.1可得,2i<=n,则i的左孩子为2i。
-> 性质5.3
由5.2可的2i其实就是i的左孩子,则2i+1就是i的右孩子,那么2i+1>n,即超过了最大的结点数,所有没有右孩子。

二叉树的存储结构

完全二叉树

二叉树遍历

二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问有且仅有一次。

三种遍历次序:

完全二叉树1
class BinTNode<T>: NSObject{
    var data: T
    var lChild: BinTNode<T>?
    var rChild: BinTNode<T>?
    init(_ data: T) {
        self.data = data
    }
}

//前序遍历
func preOrderTraverse(t: BinTNode<String>?) {
    guard let tNode = t else {
        return
    }
    
    print(tNode.data)
    preOrderTraverse(t: tNode.lChild)
    preOrderTraverse(t: tNode.rChild)
}
前序遍历结果:ABDHIEJCFG => A->(B->(D->H->I)->(E->J))->(C->F->G)

//中序遍历
func inOrderTraverse(t: BinTNode<String>?) {
    guard let tNode = t else {
        return
    }
    inOrderTraverse(t: tNode.lChild)
    print(tNode.data)
    inOrderTraverse(t: tNode.rChild)
}
中序遍历结果:HDIBJEAFCG => ((H<-D->I)<-B->(J<-E))<-A->(F<-C->G)

//后序遍历
func postOrderTraverse(t: BinTNode<String>?) {
    guard let tNode = t else {
        return
    }
    postOrderTraverse(t: tNode.lChild)
    postOrderTraverse(t: tNode.rChild)
    print(tNode.data)
}
后序遍历结果:HIDJEBFGCA => ((H->I->D)->(J->E)->B)->(F->G->C)->A

//层叙遍历
func layerOrderTraverse(t: BinTNode<String>?) {
    guard let tNode = t else {
        return
    }
    
    var array: [BinTNode<String>] = [tNode]
    while !array.isEmpty {
        let count = array.count
        for i in 0...count-1 {
            let node = array[i]
            print(node.data)
            if let lNode = node.lChild {
                array.append(lNode)
            }
            if let rNode = node.rChild {
                array.append(rNode)
            }
        }
        array.removeSubrange(Range.init(NSRange(location: 0, length: count))!)
    }
}
层序遍历结果:ABCDEFGHIJ => A->(B->C)->(D->E->F->G)->(H->I->J)

线索二叉树

线索二叉树:指向前驱和后续的指针称为线索,加上线索的二叉树称为线索二叉树。

线索二叉树的数据结构:

class BinTNode<T>: NSObject {
    let data: T
    var lChild: BinTNode<T>?  
    var rChild: BinTNode<T>? 
    var lTag: Bool = false  //lTag = true,lChild指向前驱;否则,指向左子树
    var rTag: Bool = false  //rTag = true,rChild指向后续;否则,指向右子树
    init(_ data: T) {
        self.data = data
    }
}

以上面的完全二叉树1为例:


线索二叉树

以中序遍历线索化:

//中序遍历
func inOrderTraverse_Thread(t: BinTNode<String>?) {
    var p = t?.lChild
    while p != t {
        while p?.lTag == false {  //二叉树最左边
            p = p?.lChild
        }
        print(p?.data ?? "")  //打印数据
        while p?.rChild != t, p?.rTag == true {
            p = p?.rChild
            print(p?.data ?? "")
        }
        p = p?.rChild
    }
}
核心实现思路:
1.遍历二叉树的最左结点,然后输出
2.通过后续指针获取双亲结点,然后输出
3.遍历到右子树,重复1,2两步骤

如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后续,那么采取线索二叉链表的存储结构就是非常不错的选择。

树、森林与二叉树的转换

1.加线。在所有兄弟结点之间加一条线。
2.去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子之间的连线。
3.层次调整。以树的根结点为轴心,将整个树顺时针旋转一定的角度,使之结构层次分明。
树 -> 二叉树
1.把每棵树转换为二叉树。
2.第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
森林 -> 二叉树
1.加线。若某个结点的左孩子结点存在,则将这个左孩子的n个右孩子结点都作为此结点的孩子。
2.去线。删除原二叉树中所有结点与其右孩子结点的连线。
3.层次调整。使之结构层次分明。
二叉树 -> 树
1.去线。从根结点开始,若右孩子存在,则把与右孩子相连的线去调,再看分离的二叉树,继续上个步骤。
2.将每棵分离的二叉树再转换成树。
二叉树 -> 森林
树的遍历:
1.先根遍历:
先访问根结点,然后依次先根遍历每个子树。
2.后根遍历:
先依次后根遍历每课子树,再访问根结点。
比如:图二叉树 -> 树,先根遍历:ABEFCDG  后根遍历:EFBCGDA

森林的遍历:
1.前序遍历:
使用先根依次遍历每课树。
2.后序遍历:
使用后根依次遍历每棵树。
比如:图二叉树 -> 森林,前序遍历:ABCDEFGHJI 后序遍历:BCDAFEJHIG

赫夫曼树

赫夫曼树:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。树的路径长度就是从树根到每一结点的路径长度之和。其中,带权路径长度WPL最小的二叉树称作赫夫曼树。

赫夫曼树

二叉树a中,根结点到A的路径为1,带权路径为:15x1=15
所以,二叉树a的路径为:1+2+3+4+4 = 14;二叉树b的路径为:3+3+2+2+2 = 12
二叉树a的带权路径为:1x5 + 2x15 + 3x40 + 4x30 + 4x10 = 315
二叉树b的带权路径为:3x5 + 3x15 + 2x40 + 2x30 + 2x10 = 220
如果有10000名学生需要计算成绩,则二叉树a需要判断:(315÷100)x10000 = 31500次;二叉树b需要判断:(220÷100)x10000 = 22000次,后者是前者的2/3。所以,赫夫曼树可以很有效的把比较的次数降低,加快计算的速度。赫夫曼编码,也是一种压缩编发方式,可以很有效的减少目标的大小而不丢失信息。

步骤:
1.先把结点按照权值的大小排成一列:A5,E10,B15,D30,C40。
2.取权值最小的两个结点A5和E10作为一个新结点N₁的两个子结点,权值较小的作为左子树,较大的作为右子树。然后,N₁的权值为两个子结点的权值之和:5+10=15。
        N₁
      /    \
     A      E
3.将N1代替A5和E10插入前面的有序序列:N₁15,B15,D30,C40。
4.重复步骤2,直到遍历所有的结点,得到赫夫曼树。
        T
     𝟜𝟘/  \𝟞𝟘
     C     N₃
       𝟛𝟘 /  \𝟛𝟘
        N₂    D
     𝟙𝟝/  \𝟙𝟝
     N₁    B
   𝟝/  \𝟙𝟘
   A    E
WPL:40*1 + 60*1 + 30*2 + 15*3 + 5*4 + 10*4 = 205

六. 图

图:由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V, E),其中G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

特点:

各种图定义

图的顶点和边之间的关系

图1

图的抽象数据类型

ADT 图(Graph) 
Data
    顶点是有穷且非空集合+边的集合
Operation
    ...
    addVertex(v):添加顶点
    addEdge(v1, v2):添加边
    DFSTraverse():深度优先遍历
    BFSTraverse():广度优先遍历
    ...
EndADT

图的数据存储

邻接矩阵:用两个数组来表示图。一个一维数组储存图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。

邻接矩阵图2

信息:
(1)v1的度即为第1行,所有元素之和:1+0+1+0=2。
(2)v1的邻接点即为第1行,所有元素为1的顶点。
(3)判断v1到v2是否有边,只需要判断arc[1][2]==1?就行,所以,v1到v2有边。

邻接表:数组和链表组合的存储方法称为邻接表。

邻接表处理:
(1)图中顶点使用一个一维数组存储。
(2)图中每个顶点v的所有邻接点构成一个线性表。

十字链表:即把邻接表与逆邻接表结合起来。(有向邻接表的优化)

举个例子:


十字链表图1

v0的入度为:1->0, 2->0,v0的出度为:0->3。

邻接多重表:改造边结点,使删除某一条边不需要遍历两次。

邻接多重表结点

其中ivex和jvex是与某条边依附的两个顶点在顶点表中的下标。ilink指向依附顶点ivex的下一条边,jlink指向依附顶点jvex的下一条边。

邻接多重表

上图总共有5条边,分别是v0->v1,v1->v2,v2->v3,v3->v0,v0-v2,所以,会有5个边结点。然后从v0开始,有三条邻接的边,分别是v0-v1,v0-v2,v0-v3,首先vo指向E(0,1),然后当前的边结点的ivex=0,所以,ilink需要找到下一条顶点为v0的边,顺着边结点往下找,可以找到E(3,0),所以,ilink指向E(3,0),由于,v0还有另外一条边E(0,2),所以,jlink顺着往下找,找到E(0,2),再往下没有边结点了,所以置为空。其他顶点的操作也是相似的过程。

边集数组:边集数组是由两个一位数组组成。一个是存储顶点信息,另一个是存储边的信息。这个数组每个数据元素由一条边的起点下标(begin),终点下标(end)和权(weight)组成。

边集数组

图的遍历

图的遍历:从图中某一顶点出发访遍图中其余顶点,且使每一个顶点被访问一次。

深度优先遍历图1

邻接矩阵存储图,深度优先遍历实现算法

使用深度优先遍历图1实现深度优先遍历算法
//邻接矩阵
public struct Graph<T: Equatable> {
    fileprivate var vexs: [T] = []
    fileprivate var arc: [[Int]] = []
    public var vertexes: Int {
        return vexs.count
    }
    public var edges: Int {
        var count = 0
        for items in arc {
            for item in items {
                if item > 0 {
                    count += 1
                }
            }
        }
        return count
    }
    
    public mutating func addVertex(vertext: T) {
        vexs.append(vertext)
        for i in 0..<arc.count {
            var temp = arc[i]
            temp.append(0)
            arc[i] = temp
        }
        
        let newArray = Array(repeating: 0, count: arc.count+1)
        arc.append(newArray)
    }
    
    public mutating func addEdge(from top1: T, to top2: T) {
        let containTop1 = vexs.contains { $0 == top1 }
        let containTop2 = vexs.contains { $0 == top2 }
        
        guard containTop1, containTop2 else {
            return
        }
        
        let row = vexs.index(of: top1)!
        let column = vexs.index(of: top2)!
        arc[row][column] = 1
    }
}

var graphics = Graph<String>()
graphics.addVertex(vertext: "A")
graphics.addVertex(vertext: "B")
graphics.addVertex(vertext: "C")
graphics.addVertex(vertext: "D")
graphics.addVertex(vertext: "E")
graphics.addVertex(vertext: "F")
graphics.addVertex(vertext: "G")
graphics.addVertex(vertext: "H")
graphics.addVertex(vertext: "I")

graphics.addEdge(from: "A", to: "B")
graphics.addEdge(from: "A", to: "F")
graphics.addEdge(from: "B", to: "C")
graphics.addEdge(from: "B", to: "I")
graphics.addEdge(from: "B", to: "G")
graphics.addEdge(from: "C", to: "I")
graphics.addEdge(from: "C", to: "D")
graphics.addEdge(from: "D", to: "I")
graphics.addEdge(from: "D", to: "E")
graphics.addEdge(from: "D", to: "G")
graphics.addEdge(from: "D", to: "H")
graphics.addEdge(from: "E", to: "F")
graphics.addEdge(from: "E", to: "H")
graphics.addEdge(from: "F", to: "G")
graphics.addEdge(from: "G", to: "H")

//深度优先遍历
var visited: [Bool] = []
func DFS(graphics: Graph<String>, i: Int) {
    visited[i] = true
    print(graphics.vexs[i])
    
    for j in 0..<graphics.vertexes {
        if graphics.arc[i][j] == 1, !visited[j] {
            DFS(graphics: graphics, i: j)
        }
    }
}

func DFSTraverse(graphics: Graph<String>) {
    for _ in 0..<graphics.vertexes {
        visited.append(false)
    }
    
    for i in 0..<graphics.vertexes {
        if !visited[i] {
            DFS(graphics: graphics, i: i)
        }
    }
}

DFSTraverse(graphics: graphics)

输出结果:ABCDEFGHI

邻接链表存储图,深度优先遍历实现算法:

class EdgeNode<T: Equatable> {
    var index: Int
    var nextEdge: EdgeNode<T>?

    init(atIndex index: Int) {
        self.index = index
    }
}

class VertexNode<T: Equatable> {
    var data: T
    var firstEdge: EdgeNode<T>?

    init(_ data: T) {
        self.data = data
    }
}

class Graphics<T: Equatable> {
    var adjList: [VertexNode<T>] = []
    var numVertexes: Int {
        return adjList.count
    }
    var numEdges: Int {
        var count = 0
        for vertex in adjList {
            if let edge = vertex.firstEdge {
                count += 1
                var next = edge.nextEdge
                while next != nil {
                    count += 1
                    next = next?.nextEdge
                }
            }
        }
        return count/2
    }

    func addVertex(vertex: T) {
        let v = VertexNode<T>(vertex)
        adjList.append(v)
    }

    func addEdge(from v1: T, to v2: T) {
        let vertex1 = getVertex(data: v1)
        let vertex2 = getVertex(data: v2)
        guard vertex1 != nil, vertex2 != nil else {
            return;
        }

        let index1 = getVertextIndex(v1)!
        let index2 = getVertextIndex(v2)!
        
        vertextAddEdge(vertex: vertex1!, index: index2)
        vertextAddEdge(vertex: vertex2!, index: index1)
    }

    func vertextAddEdge(vertex: VertexNode<T>, index: Int) {
        var edgeNode = vertex.firstEdge
        guard edgeNode != nil else {
            vertex.firstEdge = EdgeNode<T>(atIndex: index)
            return;
        }

        let value = adjList[index].data
        if adjList[(edgeNode?.index)!].data == value {
            return
        }
        while edgeNode?.nextEdge != nil {
            edgeNode = edgeNode?.nextEdge
            if adjList[(edgeNode?.index)!].data == value {
                return
            }
        }
        edgeNode?.nextEdge = EdgeNode<T>(atIndex: index)
    }

    func getVertex(data: T) -> VertexNode<T>? {
        for v in adjList {
            if v.data == data {
                return v
            }
        }
        return nil
    }

    func getVertextIndex(_ data: T) -> Int? {
        for index in 0..<adjList.endIndex {
            let vertex = adjList[index]
            if vertex.data == data {
                return index
            }
        }
        return nil
    }
}

let graphics = Graphics<String>()
graphics.addVertex(vertex: "A")
graphics.addVertex(vertex: "B")
graphics.addVertex(vertex: "C")
graphics.addVertex(vertex: "D")
graphics.addVertex(vertex: "E")
graphics.addVertex(vertex: "F")
graphics.addVertex(vertex: "G")
graphics.addVertex(vertex: "H")
graphics.addVertex(vertex: "I")

graphics.addEdge(from: "A", to: "B")
graphics.addEdge(from: "A", to: "F")
graphics.addEdge(from: "B", to: "A")
graphics.addEdge(from: "B", to: "C")
graphics.addEdge(from: "B", to: "G")
graphics.addEdge(from: "B", to: "I")
graphics.addEdge(from: "C", to: "B")
graphics.addEdge(from: "C", to: "D")
graphics.addEdge(from: "C", to: "I")
graphics.addEdge(from: "D", to: "C")
graphics.addEdge(from: "D", to: "E")
graphics.addEdge(from: "D", to: "G")
graphics.addEdge(from: "D", to: "H")
graphics.addEdge(from: "D", to: "I")
graphics.addEdge(from: "E", to: "D")
graphics.addEdge(from: "E", to: "F")
graphics.addEdge(from: "E", to: "H")
graphics.addEdge(from: "F", to: "G")
graphics.addEdge(from: "G", to: "B")
graphics.addEdge(from: "G", to: "D")
graphics.addEdge(from: "G", to: "F")
graphics.addEdge(from: "G", to: "H")

var visited: [Bool] = []
func DFS(graphics: Graphics<String>, i: Int) {
    visited[i] = true
    print(graphics.adjList[i].data)

    var edgeNode = graphics.adjList[i].firstEdge
    while edgeNode != nil {
        if !visited[(edgeNode?.index)!] {
            DFS(graphics: graphics, i: (edgeNode?.index)!)
        }
        edgeNode = edgeNode?.nextEdge
    }
}

func DFSTraverse(graphics: Graphics<String>) {
    for _ in 0..<graphics.numVertexes {
        visited.append(false)
    }

    for i in 0..<graphics.numVertexes {
        if !visited[i] {
            DFS(graphics: graphics, i: i)
        }
    }
}

DFSTraverse(graphics: graphics)
输出结果:ABCDEFGHI

邻接矩阵存储图,广度优先遍历实现算法

var visited: [Bool] = []

func BFSTraverse(graphics: Graph<String>) {
    for _ in 0..<graphics.vertexes {
        visited.append(false)
    }
    
    var result: [String] = []
    for i in 0..<graphics.vertexes {
        if !visited[i] {
            visited[i] = true
            print(graphics.vexs[i])
            result.append(graphics.vexs[i])
            while !result.isEmpty {
                result.removeFirst()
                for j in 0..<graphics.vertexes {
                    if graphics.arc[i][j] == 1, !visited[j] {
                        visited[j] = true
                        print(graphics.vexs[j])
                        result.append(graphics.vexs[j])
                    }
                }
            }
        }
    }
}

BFSTraverse(graphics: graphics)
输出结果:ABFCDIEHG

邻接链表存储图,深度优先遍历实现算法:

var visited: [Bool] = []
func BFSTraverse(graphics: Graphics<String>) {
    for _ in 0..<graphics.numVertexes {
        visited.append(false)
    }
    
    var result: [VertexNode<String>] = []
    for i in 0..<graphics.numVertexes {
        if !visited[i] {
            visited[i] = true
            print(graphics.adjList[i].data)
            result.append(graphics.adjList[i])
            while !result.isEmpty {
                let vertex = result.removeFirst()
                var edgeNode = vertex.firstEdge
                while edgeNode != nil {
                    if !visited[(edgeNode?.index)!] {
                        visited[(edgeNode?.index)!] = true
                        print(graphics.adjList[(edgeNode?.index)!].data)
                        result.append(graphics.adjList[(edgeNode?.index)!])
                    }
                    edgeNode = edgeNode?.nextEdge
                }
            }
        }
    }
}

BFSTraverse(graphics: graphics)
输出结果:ABFCGIEDH

比较:深度优先遍历适合目标比较明确,而广度优先遍历更适合在不断扩大范围时找到相对最优解的情况。

最小生成树

最小生成树:构造连通网的最小代价生成树称为最小生成树。

寻找最小生成树的两种算法:普里姆算法(Prim)和克鲁斯卡尔算法(Kruskal)。

假设N是连通网,TE是N上最小生成树中边的集合。算法从U={u0}(u0∈V),TE={}开始。重复执行下述操作:在所有u∈V,v∈V-U的边(u,v)∈E中,找一条代价最小的边(u0,v0)并入集合TE,同时v0并入U,直至U=V为止。此时TE中必有n-1条边,则T=(V,{TE})为N的最小生成树,时间复杂度为O(n^2)。通俗来讲,先从v0开始,找到和v0连接的权重最小的边的顶点v1,然后再找{v0,v1}相连的权重最小的边,直到所有顶点都找完。

func miniSpanTree_prim(graphics: Graph<String>) {
    var adjvex = Array(repeating: 0, count: graphics.vertexes)  //初始化都为0,意味着从v0开始找
    var lowcost = graphics.arc[0] //初始化为v0所关联的所有边的权值
    
    //因为v0已经在adjvex里面了,所以从下标1开始寻找
    var min: Int
    for _ in 1..<graphics.vertexes {
        min = Int.max  //lowcost下标为0则说明该顶点已被找过,下标为Int.max则意味着,没有该条边
        var k = 0 //保存最小值的下标
        for j in 1..<graphics.vertexes {
            if lowcost[j] != 0, lowcost[j] < min {
                min = lowcost[j]
                k = j
            }
        }
        print("\(adjvex[k], k)")  //adjvex[k]为当前的顶点,k为当前顶点关联权值最小边的顶点的下标
        //将找到的最小权值边的下一个顶点加入adjvex,并且将该顶点相关联的较小边加入lowcost
        for j in 1..<graphics.vertexes {
            if lowcost[j] != 0, graphics.arc[k][j] < lowcost[j] {
                lowcost[j] = graphics.arc[k][j]
                adjvex[j] = k
            }
        }
    }
}

miniSpanTree_prim(graphics: graphics)
输出结果:
(0, 1)
(0, 5)
(1, 8)
(8, 2)
(1, 6)
(6, 7)
(7, 4)
(7, 3)

假设 N= (V,{E})是连通网,则令最小生成树的初始状态为只有 n 个顶点而无边的 非连通图 T={V,{}},图中每个顶点自成一个连通分量。在 E 中选择代价最小的边,若 该边依附的顶点落在 T 中不同的连通分量上,则将此边加入到 T 中,否则舍去此边而 选择下一条代价最小的边。依次类推,直至 T 中所有顶点都在罔一连通分量上为止 ,时间复杂度为O(eloge)。通俗来讲就是,先把所有的边按照权值的大小排序,然后从最小的开始选择,每选择一次需要判断是否和之前已选择的边形成回路,如果形成回路则选择下一条边,直到所有的边都被遍历完,所得就是最小生成树。

struct Edge{
    let begin: Int, end: Int, weight: Int
    init (begin: Int, end: Int, weight: Int) {
        self.begin = begin
        self.end = end
        self.weight = weight
    }
}

func miniSpanTree_Kruskal(graphics: Graph<String>) {
    var edges: [Edge] = []  //边集数组
    for i in 1..<graphics.vertexes {
        let arc = graphics.arc[i-1]
        for j in i..<graphics.vertexes {
            if arc[j] < Int.max {
                let edge = Edge(begin: i-1, end: j, weight: arc[j])
                edges.append(edge)
            }
        }
    }
    edges.sort{$0.weight < $1.weight}
    var parent = Array(repeating: 0, count: graphics.vertexes)  //判断是否成环的数组
    for i in 0..<edges.count {
        let n = find(parent: parent, fedge: edges[i].begin)
        let m = find(parent: parent, fedge: edges[i].end)
        if n != m {  //一条边从两边开始计算,只要最后的点相等,则会构成环
            parent[n] = m
            print("\(edges[i].begin, edges[i].end) \(edges[i].weight)")
        }
    }
}

//寻找每个点的路径
func find(parent: [Int], fedge: Int) -> Int {
    var result = fedge
    while parent[result] > 0 {
        result = parent[result]
    }
    return result
}

miniSpanTree_Kruskal(graphics: graphics)

最短路径

对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最小的路径,并且,我们称路径上第一个顶点为源点,最后一个顶点为终点。

以下面的网图,寻找最短路径:

最短路径
var graphics = Graph<String>()
graphics.addVertex(vertext: "v0")
graphics.addVertex(vertext: "v1")
graphics.addVertex(vertext: "v2")
graphics.addVertex(vertext: "v3")
graphics.addVertex(vertext: "v4")
graphics.addVertex(vertext: "v5")
graphics.addVertex(vertext: "v6")
graphics.addVertex(vertext: "v7")
graphics.addVertex(vertext: "v8")

//最短路径
graphics.addEdge(from: "v0", to: "v1", weight: 1)
graphics.addEdge(from: "v0", to: "v2", weight: 5)

graphics.addEdge(from: "v1", to: "v0", weight: 1)
graphics.addEdge(from: "v1", to: "v2", weight: 3)
graphics.addEdge(from: "v1", to: "v3", weight: 7)
graphics.addEdge(from: "v1", to: "v4", weight: 5)

graphics.addEdge(from: "v2", to: "v0", weight: 5)
graphics.addEdge(from: "v2", to: "v1", weight: 3)
graphics.addEdge(from: "v2", to: "v4", weight: 1)
graphics.addEdge(from: "v2", to: "v5", weight: 7)

graphics.addEdge(from: "v3", to: "v1", weight: 7)
graphics.addEdge(from: "v3", to: "v4", weight: 2)
graphics.addEdge(from: "v3", to: "v6", weight: 3)

graphics.addEdge(from: "v4", to: "v1", weight: 5)
graphics.addEdge(from: "v4", to: "v2", weight: 1)
graphics.addEdge(from: "v4", to: "v3", weight: 2)
graphics.addEdge(from: "v4", to: "v5", weight: 3)
graphics.addEdge(from: "v4", to: "v6", weight: 6)
graphics.addEdge(from: "v4", to: "v7", weight: 9)

graphics.addEdge(from: "v5", to: "v2", weight: 7)
graphics.addEdge(from: "v5", to: "v4", weight: 3)
graphics.addEdge(from: "v5", to: "v7", weight: 5)

graphics.addEdge(from: "v6", to: "v3", weight: 3)
graphics.addEdge(from: "v6", to: "v4", weight: 6)
graphics.addEdge(from: "v6", to: "v7", weight: 2)
graphics.addEdge(from: "v6", to: "v8", weight: 7)

graphics.addEdge(from: "v7", to: "v4", weight: 9)
graphics.addEdge(from: "v7", to: "v5", weight: 5)
graphics.addEdge(from: "v7", to: "v6", weight: 2)
graphics.addEdge(from: "v7", to: "v8", weight: 4)

graphics.addEdge(from: "v8", to: "v6", weight: 7)
graphics.addEdge(from: "v8", to: "v7", weight: 4)

从源点开始,找到源点到下一个最短路径的顶点。然后,在之前的基础上,再找下一个最短路径的顶点,直到终点。该算法的时间复杂度为O(n^2)。

func shortestPath_Dijkstra(graphics: Graph<String>) {
    var final = Array(repeating: 0, count: graphics.vertexes)  //final[N]=1,表示求的顶点v0至vN的最短路径
    var pathArc = Array(repeating: 0, count: graphics.vertexes)  //储存最短路径下标前驱的数组
    var shortPathTable: [Int] = []  //表示v0到各个顶点的最短路径长度
    for i in 0..<graphics.vertexes {
        shortPathTable.append(graphics.arc[0][i])
    }
    final[0] = 1  //v0-v0不需要求路径
    //开始求v0到各个顶点的最短路径
    for _ in 1..<graphics.vertexes {
        var min = Int.max  //当前所知离v0顶点最近的距离
        var k = 0
        for w in 0..<graphics.vertexes {
            if final[w] == 0, shortPathTable[w] < min {
                k = w
                min = shortPathTable[w]
            }
        }
        final[k] = 1
        //修正最短路径
        for w in 0..<graphics.vertexes {
            if final[w] == 0, min < shortPathTable[w]-graphics.arc[k][w]{
                shortPathTable[w] = min + graphics.arc[k][w]
                pathArc[w] = k
            }
        }
    }
    
    print(final)
    print(pathArc)
    print(shortPathTable)
}

shortestPath_Dijkstra(graphics: graphics)
//输出结果
[1, 1, 1, 1, 1, 1, 1, 1, 1]
[0, 0, 1, 4, 2, 4, 3, 6, 7]
[0, 1, 4, 7, 5, 8, 10, 12, 16]

关键公式:D[ v ][ w ] = min { D[ v ][ w ] , D[ V ][0] + D[0][W] },主要思想就是:求v1到v2经过最短路径,有v1->v2,v1->v0->v2,则v1->v2的最短路径为两者最短的路径。以此类推,在其基础上求v1-v3的路径也是一样的。算法时间复杂度为O(n^3)。

//佛洛伊德算法
func ShortestPath_Floyd(graphics: Graph<String>) {
    //初始化矩阵
    var pathmatirx: [[Int]]
    var shortPathTable: [[Int]] = graphics.arc
    
    var rowValue: [Int] = []
    for i in 0..<graphics.vertexes {
        rowValue.append(i)
    }
    pathmatirx = Array(repeating: rowValue, count: graphics.vertexes)
    
    //开始算法
    for k in 0..<graphics.vertexes {
        for v in 0..<graphics.vertexes {
            for w in 0..<graphics.vertexes {
                if shortPathTable[v][k] < Int.max, shortPathTable[k][w] < Int.max, shortPathTable[v][w] > shortPathTable[v][k] + shortPathTable[k][w] {
                    shortPathTable[v][w] = shortPathTable[v][k] + shortPathTable[k][w]
                    pathmatirx[v][w] = pathmatirx[v][k]
                }
            }
        }
    }
    
    //打印最短路径
    var k = 0
    for v in 0..<graphics.vertexes {
        for w in v+1..<graphics.vertexes {
            print("v\(v)-v\(w) weight: \(shortPathTable[v][w])")
            k = pathmatirx[v][w]
            print("path: \(v)")
            while k != w {
                print(" -> \(k)")
                k = pathmatirx[k][w]
            }
            print(" -> \(w)")
        }
        print("\n")
    }
}

ShortestPath_Floyd(graphics: graphics)

//输出结果
[0, 1, 4, 7, 5, 8, 10, 12, 16]   //v0达到各顶点的最短路径
[1, 0, 3, 6, 4, 7, 9, 11, 15]
[4, 3, 0, 3, 1, 4, 6, 8, 12]
[7, 6, 3, 0, 2, 5, 3, 5, 9]
[5, 4, 1, 2, 0, 3, 5, 7, 11]
[8, 7, 4, 5, 3, 0, 7, 5, 9]
[10, 9, 6, 3, 5, 7, 0, 2, 6]
[12, 11, 8, 5, 7, 5, 2, 0, 4]
[16, 15, 12, 9, 11, 9, 6, 4, 0]
//前驱矩阵
[0, 1, 1, 1, 1, 1, 1, 1, 1]
[0, 1, 2, 2, 2, 2, 2, 2, 2]
[1, 1, 2, 4, 4, 4, 4, 4, 4]
[4, 4, 4, 3, 4, 4, 6, 6, 6]
[2, 2, 2, 3, 4, 5, 3, 3, 3]
[4, 4, 4, 4, 4, 5, 7, 7, 7]
[3, 3, 3, 3, 3, 7, 6, 7, 7]
[6, 6, 6, 6, 6, 5, 6, 7, 8]
[7, 7, 7, 7, 7, 7, 7, 7, 8]

拓扑结构

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图表示的网,称为AOV网(Activity On vertex Network)。假设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列v1, v2, v3, ...... , Vn,满足若从顶点vi到vj有一条路径,则中顶点序列中顶点vi必须在vj之前,我们称这样的顶点序列为一个拓扑序列。拓扑排序,就是对一个有向图构造拓扑序列的过程。拓扑序列的主要作用是确定一个有向图确定的有序序列,比如:iOS开发使用拓扑序列来整理整个工程的文件依赖,比如检查循环依赖,比如确认从那个依赖文件开始编译等等。两个特点:1.无环有向图 2.两个顶点之间的方向只有一个。

AVO图.png AVO图链表表示.png
extension Int {
    static func += (left: inout Int, right: Int) {
        left = left + right
    }
    
    static func -= (left: inout Int, right: Int) {
        left = left - right
    }
}

//边表结点
class EdgeNode {
    var adjex = 0
    var weight = 1
    var next: EdgeNode?
}

//顶点表结点
class VertexNode {
    var inEdge = 0
    var data = 0
    var firstEdge: EdgeNode?
}

class GraphAdjList {
    var adjList: [VertexNode] = []
    var numEdges = 0
    var numVertexes : Int {
        return adjList.count
    }
}

let graphiAdjList = GraphAdjList()

//V0
let v0 = VertexNode()
v0.inEdge = 0
v0.data = 0

let v0E11 = EdgeNode()
v0E11.adjex = 11
let v0E5 = EdgeNode()
v0E5.adjex = 5
let v0E4 = EdgeNode()
v0E4.adjex = 4

v0.firstEdge = v0E11
v0E11.next = v0E5
v0E5.next = v0E4

//V1
let v1 = VertexNode()
v1.inEdge = 0
v1.data = 1

let v1E8 = EdgeNode()
v1E8.adjex = 8
let v1E4 = EdgeNode()
v1E4.adjex = 4
let v1E2 = EdgeNode()
v1E2.adjex = 2

v1.firstEdge = v1E8
v1E8.next = v1E4
v1E4.next = v1E2

//V2
let v2 = VertexNode()
v2.inEdge = 2
v2.data = 2

let v2E9 = EdgeNode()
v2E9.adjex = 9
let v2E6 = EdgeNode()
v2E6.adjex = 6
let v2E5 = EdgeNode()
v2E5.adjex = 5

v2.firstEdge = v2E9
v2E9.next = v2E6
v2E6.next = v2E5

//V3
let v3 = VertexNode()
v3.inEdge = 0
v3.data = 3

let v3E13 = EdgeNode()
v3E13.adjex = 13
let v3E2 = EdgeNode()
v3E2.adjex = 2

v3.firstEdge = v3E13
v3E13.next = v3E2

//V4
let v4 = VertexNode()
v4.inEdge = 2
v4.data = 4

let v4E7 = EdgeNode()
v4E7.adjex = 7

v4.firstEdge = v4E7

//V5
let v5 = VertexNode()
v5.inEdge = 3
v5.data = 5

let v5E12 = EdgeNode()
v5E12.adjex = 12
let v5E8 = EdgeNode()
v5E8.adjex = 8

v5.firstEdge = v5E12
v5E12.next = v5E8

//V6
let v6 = VertexNode()
v6.inEdge = 1
v6.data = 6

let v6E5 = EdgeNode()
v6E5.adjex = 5

v6.firstEdge = v6E5

//V7
let v7 = VertexNode()
v7.inEdge = 2
v7.data = 7

//V8
let v8 = VertexNode()
v8.inEdge = 2
v8.data = 8

let v8E7 = EdgeNode()
v8E7.adjex = 7

v8.firstEdge = v8E7

//V9
let v9 = VertexNode()
v9.inEdge = 2
v9.data = 9

let v9E11 = EdgeNode()
v9E11.adjex = 11
let v9E10 = EdgeNode()
v9E10.adjex = 10

v9.firstEdge = v9E11
v9E11.next = v9E10

//V10
let v10 = VertexNode()
v10.inEdge = 1
v10.data = 10

let v10E13 = EdgeNode()
v10E13.adjex = 13

v10.firstEdge = v10E13

//V11
let v11 = VertexNode()
v11.inEdge = 2
v11.data = 11

//V12
let v12 = VertexNode()
v12.inEdge = 1
v12.data = 12

let v12E9 = EdgeNode()
v12E9.adjex = 9

v12.firstEdge = v12E9

//V13
let v13 = VertexNode()
v13.inEdge = 2
v13.data = 13

graphiAdjList.adjList.append(v0)
graphiAdjList.adjList.append(v1)
graphiAdjList.adjList.append(v2)
graphiAdjList.adjList.append(v3)
graphiAdjList.adjList.append(v4)
graphiAdjList.adjList.append(v5)
graphiAdjList.adjList.append(v6)
graphiAdjList.adjList.append(v7)
graphiAdjList.adjList.append(v8)
graphiAdjList.adjList.append(v9)
graphiAdjList.adjList.append(v10)
graphiAdjList.adjList.append(v11)
graphiAdjList.adjList.append(v12)
graphiAdjList.adjList.append(v13)

func toPologicalSort(gl: GraphAdjList) {
    var count = 0, top = 0, k = 0, getTop = 0
    var stack: [Int] = []
    //将有向图所有入度为0的顶点入栈
    for i in 0..<gl.numVertexes {
        if gl.adjList[i].inEdge == 0 {
            stack.append(i)
            top += 1
        }
    }
    //开始算拓扑序列
    while top != 0 {
        getTop = stack.removeLast()
        top -= 1
        print("vertex: \(gl.adjList[getTop].data)")
        count += 1
        var edgeNode = gl.adjList[getTop].firstEdge
        while edgeNode != nil {
            k = (edgeNode?.adjex)!
            gl.adjList[k].inEdge -= 1
            if gl.adjList[k].inEdge == 0 {
                stack.append(k)
                top += 1
            }
            edgeNode = edgeNode?.next
        }
    }
    
    if count < gl.numVertexes {
        print("Error")
    } else {
        print("OK")
    }
}

toPologicalSort(gl: graphiAdjList)
//输出结果
vertex: 3
vertex: 1
vertex: 2
vertex: 6
vertex: 0
vertex: 4
vertex: 5
vertex: 8
vertex: 7
vertex: 12
vertex: 9
vertex: 10
vertex: 13
vertex: 11

1.先找到所有入度为0的顶点,并依次进栈。
2.从栈顶开始,出栈;然后,遍历该栈顶元素的链表,每个顶点的入度减1,判断每个顶点的入度是否为0,如果为0则入栈,否则跳过。
3.判断栈是否还有元素,如果栈不为空,则回到第2步,否则,结束。

关键路径

拓扑排序解决一个工程能否顺利进行的问题,那么,关键路径就是解决工程完成需要的最短时间问题。在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为 AOE网 (Activity On Edge Network) 。 我们把AOE网中没有人边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。

AOE图.png
AOE链表显示.png
//V0
let v0 = VertexNode()
v0.inEdge = 0
v0.data = 0

let v0E2 = EdgeNode()
v0E2.adjex = 2
v0E2.weight = 4
let v0E1 = EdgeNode()
v0E1.adjex = 1
v0E1.weight = 3

v0.firstEdge = v0E2
v0E2.next = v0E1

//V1
let v1 = VertexNode()
v1.inEdge = 1
v1.data = 1

let v1E4 = EdgeNode()
v1E4.adjex = 4
v1E4.weight = 6
let v1E3 = EdgeNode()
v1E3.adjex = 3
v1E3.weight = 5

v1.firstEdge = v1E4
v1E4.next = v1E3

//V2
let v2 = VertexNode()
v2.inEdge = 1
v2.data = 2

let v2E5 = EdgeNode()
v2E5.adjex = 5
v2E5.weight = 7
let v2E3 = EdgeNode()
v2E3.adjex = 3
v2E3.weight = 8

v2.firstEdge = v2E5
v2E5.next = v2E3

//V3
let v3 = VertexNode()
v3.inEdge = 2
v3.data = 3

let v3E4 = EdgeNode()
v3E4.adjex = 4
v3E4.weight = 3

v3.firstEdge = v3E4

//V4
let v4 = VertexNode()
v4.inEdge = 2
v4.data = 4

let v4E7 = EdgeNode()
v4E7.adjex = 7
v4E7.weight = 4
let v4E6 = EdgeNode()
v4E6.adjex = 6
v4E6.weight = 9

v4.firstEdge = v4E7
v4E7.next = v4E6

//V5
let v5 = VertexNode()
v5.inEdge = 1
v5.data = 5

let v5E7 = EdgeNode()
v5E7.adjex = 7
v5E7.weight = 6

v5.firstEdge = v5E7

//V6
let v6 = VertexNode()
v6.inEdge = 1
v6.data = 6

let v6E9 = EdgeNode()
v6E9.adjex = 9
v6E9.weight = 2

v6.firstEdge = v6E9

//V7
let v7 = VertexNode()
v7.inEdge = 2
v7.data = 7

let v7E8 = EdgeNode()
v7E8.adjex = 8
v7E8.weight = 5

v7.firstEdge = v7E8

//V8
let v8 = VertexNode()
v8.inEdge = 1
v8.data = 8

let v8E9 = EdgeNode()
v8E9.adjex = 9
v8E9.weight = 3

v8.firstEdge = v8E9

//V9
let v9 = VertexNode()
v9.inEdge = 2
v9.data = 9

let graphiAdjList = GraphAdjList()

graphiAdjList.adjList.append(v0)
graphiAdjList.adjList.append(v1)
graphiAdjList.adjList.append(v2)
graphiAdjList.adjList.append(v3)
graphiAdjList.adjList.append(v4)
graphiAdjList.adjList.append(v5)
graphiAdjList.adjList.append(v6)
graphiAdjList.adjList.append(v7)
graphiAdjList.adjList.append(v8)
graphiAdjList.adjList.append(v9)

//etv(earliest time of vertex) => 顶点V的最早发生时间
//ltv(latest time of vertex) => 顶点V的最迟发生时间
//ete(earliest time of edge) => 弧a的最早发生时间
//lte(latest time of edge) => 弧a的最迟发生时间

var etv: [Int] = Array(repeating: 0, count: graphiAdjList.numVertexes)
var ltv: [Int] = []
var stack2: [Int] = []
var top2 = 0

func TopologicalSort(gl: GraphAdjList) {
    var count = 0, top = 0, k = 0, getTop = 0
    var stack: [Int] = []
    //将有向图所有入度为0的顶点入栈
    for i in 0..<gl.numVertexes {
        if gl.adjList[i].inEdge == 0 {
            stack.append(i)
            top += 1
        }
    }
    //开始算拓扑序列
    while top != 0 {
        getTop = stack.removeLast()
        top -= 1
        count += 1
        /////////////////////
        stack2.append(getTop)  //添加入度为0的顶点
        top2 += 1
        ////////////////////
        var edgeNode = gl.adjList[getTop].firstEdge
        while edgeNode != nil {
            k = (edgeNode?.adjex)!
            gl.adjList[k].inEdge -= 1
            if gl.adjList[k].inEdge == 0 {
                stack.append(k)
                top += 1
            }
            /////////////////////////////////////////////
            if etv[getTop]+(edgeNode?.weight)! > etv[k] {   //顶点的最早发生时间,即到达这个顶点之前最大的花费时间
                etv[k] = etv[getTop]+(edgeNode?.weight)!
            }
            /////////////////////////////////////////////
            edgeNode = edgeNode?.next
        }
    }
    
    if count < gl.numVertexes {
        print("Error")
    } else {
        print("OK")
    }
}

func CriticalPath(gl: GraphAdjList) {
    var getTop = 0, k = 0
    var ete = 0, lte = 0
    TopologicalSort(gl: gl)  //求拓扑序列,计算数组etv和stack2
    //   etv: [0, 3, 4, 12, 15, 11, 24, 19, 24, 27]
    //stack2: [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9]
    for _ in 0..<gl.numVertexes {
        ltv.append(etv[gl.numVertexes-1])  //初始化ltv为27
    }
    while top2 != 0 {
        getTop = stack2.removeLast()
        top2 -= 1
        var edgeNode = gl.adjList[getTop].firstEdge
        while edgeNode != nil {
            k = (edgeNode?.adjex)!
            if ltv[k]-(edgeNode?.weight)! < ltv[getTop] {
                ltv[getTop] = ltv[k]-(edgeNode?.weight)!  //取最小值,才不会影响该顶点到达其他顶点的时间,也就是最迟发生的时间
            }
            edgeNode = edgeNode?.next
        }
    }
    for j in 0..<gl.numVertexes {
        var edgeNode = gl.adjList[j].firstEdge
        while edgeNode != nil {
            k = (edgeNode?.adjex)!
            ete = etv[j]
            lte = ltv[k]-(edgeNode?.weight)!
            if ete == lte {
                print("<\(gl.adjList[j].data), \(gl.adjList[k].data) length: \((edgeNode?.weight)!)>")
            }
            edgeNode = edgeNode?.next
        }
    }
}

输出结果:
<0, 2 length: 4>
<2, 3 length: 8>
<3, 4 length: 3>
<4, 7 length: 4>
<7, 8 length: 5>
<8, 9 length: 3>

2.ltv的计算:由etv可以知道终点的最早开始时间,那么所有点以这个为初始值,因为最迟开始的时间不可能大于终点最早的开始时间。那么,ltv的计算从终点开始,往回推。比如说要计算v8最迟开始的时间,因为v8需要3天时间工作,那么在保证v8工作完成时不影响v9的工作,那么v8最迟开始的工作时间就是v9最早工作时间减去v8需要的时间。比如说v4,经过v4会到达多个顶点,需要保证不影响所达到的多个的工作时间,那么v4开始的时间就是v4所经过的过个顶点最早的开始时间分别减去v4所需要的时间,取最小的开始时间,才会不影响之后所有的顶点的最早开始时间。


ltv计算公式.png

3.ete本来是表示活动 <vk,vj>的最早开工时间,是针对弧来说
的。但只有此弧的弧尾顶点vk的事件发生了,它才可以开始,因此 ete=etv[k];lte 表示的是活动<vk,vj>的最晚开工时间,但此活动再晚也不能等 vj事件发生才开始,而必须要在vj事件之前发生,所以 lte=ltv[j]-len<vk,vj>。

总结:

写这个数据结果,断断续续奋战了几个星期,总算一点一点的把它啃完。敲代码的基础很重要,就像盖房子一样,基础不牢固,建不起高楼大厦。后面会继续推出,算法,设计模式,编译原理,操作系统等等一些列的学习笔记。这世界很大,需要保持一点好奇心😄。最后,献上Demo

上一篇下一篇

猜你喜欢

热点阅读