大部分内容来自于《大话数据结构》,代码全部使用Swift实现。至于为什么抽风写这个?😊你懂的。
1.线性表
线性表:零个或者多个数据元素的有限序列。
性质:
- 数据元素可以为空
- 数据元素有限
- 数据元素之间的逻辑结构为线性结构,也就是一对一的关系
- 数据元素类型相同
举个例子:
白羊 -> 金牛 -> 双子 -> 巨蟹 -> 狮子 -> 处女 -> 天秤 -> 射手 -> 摩羯 -> 水平 -> 双鱼
线性表的抽象数据类型:
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指向第一个有数据的节点,有的线性表会生成一个头节点,该节点不存储任何数据或者只存储该链表的长度,该节点指向第一个有数据的节点。这样做的好处就是,第一个节点的删除和插入操作和其他节点保持一致。
下面主要解释一下,插入和删除操作:
- insert
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连上指向上一个数据元素的链,自此,先的数据插入成功。
- remove
无论insert还是remove都是先拆链,然后再组合成新的数据链。
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的节点,没有任何对象指向它,在当前函数域外就外被系统回收掉。
2.栈与队列
栈:限定在表尾进行插入和删除的线性表。
栈是一种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)")
}
比较一下前面两个的差异:
常规方法:
使用一个迭代的方式,计算出结果。不需要反复调用函数,浪费内存。但是,相对于递归来说,没有那么简洁。
递归:
递归就需要不断调用函数,建立函数副本,消耗大量的内存。但是,相对于常规方法,代码简洁易懂。
为什么说递归是栈的应用呢?
递归在执行的时候,需要不断的调用自身,直到可以返回确定的值,然后,又从最后面的一层,一层层往回调,直到最上一层,这个两个操作正好对应着栈的压栈和出栈的操作。其实,编译器也是这样干的,在前行阶段,对于每一层递归,函数的局部变量,参数值和返回地址都压进栈;在退回阶段,位于栈顶的局部变量,参数值以及返回地址都被弹出,用于返回调用层次中执行代码的剩余部分,恢复调用状态。
- 四则运算
四则运算就有意思了。自己用笔和纸算数学题知道怎么做,但是计算机呢?怎么算加减乘除的?毕竟计算机只能识别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 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就可以无视我们这些在前台排队取钱的渣渣,他们有专门的通道,多么痛的领悟😓。
3.串
串:是由零个或多个字符组成的有限序列,又叫字符串。
字符串在开发的时候很常用,以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字符串匹配算法,这里就不做详细的介绍了。