大部分内容来自于《大话数据结构》,代码全部使用Swift实现。至于为什么抽风写这个?😊你懂的。
1.线性表
线性表:零个或者多个数据元素的有限序列。
性质:
- 数据元素可以为空
- 数据元素有限
- 数据元素之间的逻辑结构为线性结构,也就是一对一的关系
- 数据元素类型相同
举个例子:白羊 -> 金牛 -> 双子 -> 巨蟹 -> 狮子 -> 处女 -> 天秤 -> 射手 -> 摩羯 -> 水平 -> 双鱼线性表的抽象数据类型:ADT 线性表(List)Data线性表的数据对象集合为{a1, a2, ......, an},每一个元素的类型都是DataType。其中,除第一个元素a1外,每一个元素有且仅有一个直接前驱元素,除了最后一个元素an外,每一个元素有且仅有一个直接后续元素。数据元素之间的关系是一对一的关系。Operationcount:线性表元素个数。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 indexguard 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 = newNodenewNode.next = nodenode.previous = newNode} else {head = newNode}} else {let node = self.node(atIndex: index-1)let nextNode = self.node(atIndex: index)node?.next = newNodenewNode.previous = nodenewNode.next = nextNodenextNode?.previous = newNode}return ErrorStatus.OK}public func remove(atIndex index: Int) -> (T?, ErrorStatus) { //Remove node at indexguard !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 = nextNodenextNode?.previous = beforeNode}return (node?.data, ErrorStatus.OK)}}
⚠️注意:
这里的head指向第一个有数据的节点,有的线性表会生成一个头节点,该节点不存储任何数据或者只存储该链表的长度,该节点指向第一个有数据的节点。这样做的好处就是,第一个节点的删除和插入操作和其他节点保持一致。
下面主要解释一下,插入和删除操作:
- insert
public func insert(data: T, atIndex index: Int) -> ErrorStatus { //Insert data at indexguard 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 = newNodenewNode.next = nodenode.previous = newNode} else {head = newNode}} else {let node = self.node(atIndex: index-1)let nextNode = self.node(atIndex: index)node?.next = newNodenewNode.next = nextNodenewNode.previous = nodenextNode?.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连上指向上一个数据元素的链,自此,先的数据插入成功。
- remove
无论insert还是remove都是先拆链,然后再组合成新的数据链。
public func remove(atIndex index: Int) -> (T?, ErrorStatus) { //Remove node at indexguard !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 = nextNodenextNode?.previous = beforeNode}return (node?.data, ErrorStatus.OK)}
1.先判断是否是空链,如果是则返回,否则再判断需要删除数据的小表是否在合理范围内,如果不是则返回。
2.判断index是否等于0,如果是,则直接将head=secondNode。
3.获取beforeNode和nextNode,然后将beforeNode.next=nextNode,nextNode,previous=beforeNode,自此,下标为index的节点,没有任何对象指向它,在当前函数域外就外被系统回收掉。
2.栈与队列
栈:限定在表尾进行插入和删除的线性表。
栈是一种LIFO(Last In First Out)的线性表,也就是数据元素遵循后进先出的原则。
栈的抽象数据类型:
ADT 栈(Stack)Data具有线性表一样的性质。Operationtop:获取栈顶元素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=0F(1) = 1, n=1F(n) = F(n-1) + F(n-2), n>1
下面使用常规的方法解决这个问题:
func rabbitNumbers() {//After 40 monthsvar r0 = 0print("rabbit: \(r0)")var r1 = 1print("rabbit: \(r1)")var r: Int = 0for _ in 2..<40 {r = r0 + r1print("rabbit: \(r)")r0 = r1r1 = 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)")}
比较一下前面两个的差异:
常规方法:使用一个迭代的方式,计算出结果。不需要反复调用函数,浪费内存。但是,相对于递归来说,没有那么简洁。递归:递归就需要不断调用函数,建立函数副本,消耗大量的内存。但是,相对于常规方法,代码简洁易懂。
为什么说递归是栈的应用呢?
递归在执行的时候,需要不断的调用自身,直到可以返回确定的值,然后,又从最后面的一层,一层层往回调,直到最上一层,这个两个操作正好对应着栈的压栈和出栈的操作。其实,编译器也是这样干的,在前行阶段,对于每一层递归,函数的局部变量,参数值和返回地址都压进栈;在退回阶段,位于栈顶的局部变量,参数值以及返回地址都被弹出,用于返回调用层次中执行代码的剩余部分,恢复调用状态。
- 四则运算
四则运算就有意思了。自己用笔和纸算数学题知道怎么做,但是计算机呢?怎么算加减乘除的?毕竟计算机只能识别0和1。😊
一般手动计算数学题我们用的是中缀表达法(符号在需要运算的数字中间):
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 1case .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.pushlet advantage = OperationPriority.init(rawValue: value)?.intValue ?? 0var topAdvantage: Int = 0while let top = stack.top, top != "(" {topAdvantage = OperationPriority.init(rawValue: top)?.intValue ?? 0if 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 = 0var 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具有线性表一样的性质。Operationfront:队列第一个数据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就可以无视我们这些在前台排队取钱的渣渣,他们有专门的通道,多么痛的领悟😓。
3.串
串:是由零个或多个字符组成的有限序列,又叫字符串。
字符串在开发的时候很常用,以Objective-C为例,有可变字符串和不可变字符串,两者的实现数据结构应该有点区别;一个是链式存储结构,一个是顺序存储结构。
字符串的抽象数据类型:
ADT 串(String)Data串中元素仅由一个字符组成,相邻元素具有前驱和后续关系。OperationStringEmpty(S):若串S为空,返回true,否则返回false。...EndADT
按存储结构分类:
- 顺序存储结构的字符串
- 链式存储结构的字符串
字符串的模式匹配
朴素的模式匹配算法:核心思想,需要匹配的字符串和主串从下标0开始匹配,一旦子串不匹配,则子串又从下标0开始匹配,主串则挪到下标1,不断的循环这个过程,直到主串或者字串当前的下标超过字符串的长度或者匹配成功返回。
let s = "goodgoogle"let t = "google"func matchString(s: String, t: String) -> Int {var post: Int = -1guard !s.isEmpty || !t.isEmpty else {return post}var i = 0 //s current indexvar j = 0 //t current indexwhile 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+1j = j+1} else {i = i-j+1j = 0}}if j > t.count-1 {post = i-t.count}return post}
朴素的模式匹配,虽然,简单明了,但是效率低。所以,强大的KMP模式匹配算法就应运而生了。
强大的KMP算法,这个可以看我之前写的KMP字符串匹配算法,这里就不做详细的介绍了。
