算法数据结构
数组
1 | // Todo |
字典
1 | // Todo |
集合
1 | // Todo |
链表
链表是一个集合,它的值是线性排列的序列。链表相比一些连续存储策略,例如 Swift Array,有这么几个理论上的优势:
- 固定时间插入和顶端移除
- 可靠的特征表现
由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
双向链表,又称为双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
Node 节点
链表是一个节点的链条,节点拥有两个职责:
- 持有一个值
- 持有一个对下一节点的引用,一个 nil 代表了链表的结束
1 | public class Node<Value> { |
链表增加值
链表有一个头尾的改变,头指代了第一个节点,尾指代第二个节点。
有三种方式给一个链表添加值:
- push:在链表前端添加值
- append:在链表末尾添加值
- insert(after:):在一个特定的节点后添加值
1 | public struct LinkedList<Value> { |
性能
push | append | insert(after:) | node(at:) | |
---|---|---|---|---|
Behavior | insert at head | insert at tail | insert after a node | returns a node at given index |
Time complexity | O(1) | O(1) | O(1) | O(i) where i is the given index |
移除值
有三种操作移除链表中的节点:
- pop:移除最前端节点
- removeLast:移除尾部节点
- remove(after:):移除链表中节点
1 | extension LinkedList { |
Perform analysis
pop | removeLast | Remove(after:) | |
---|---|---|---|
Time complexity | O(1) | O(n) | O(1) |
栈
栈(英语:stack)又称为栈或堆叠,是计算机科学中一种特殊的串列形式的抽象数据类型,其特殊之处在于只能允许在链表或数组的一端(称为堆栈顶端指针,英语:top)进行加入数据(英语:push)和输出数据(英语:pop)的运算。另外栈也可以用一维数组或链表的形式来完成。堆栈的另外一个相对的操作方式称为队列。
由于堆栈数据结构只允许在一端进行操作,因而按照后进先出(LIFO, Last In First Out)的原理运作。
对于栈,只有两个必要的操作:
- push:在栈顶添加一个元素
- pop:移除栈顶元素
- 在 iOS 开发中,如果你要在你的 App 中添加撤销操作(比如删除图片,恢复删除图片),那么栈是首选数据结构
- 无论在面试还是写 App 中,只关注栈的这几个基本操作:push, pop, isEmpty, peek, size。
1 | public struct Stack<Element> { |
队列
队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。
队列所关心的操作:
- enqueue:在队列末尾插入一个元素
- dequeue:移除队列头部元素
- isEmpty:队列是否为空
- peek:返回队列最开头的元素,不移除
1 | public protocol Queue { |
基于数组的队列
操作 | 最好情况 | 最差情况 |
---|---|---|
enqueue | O(1) | O(1) |
dequeue | O(n) | O(n) |
空间复杂度 | O(n) | O(n) |
基于双向链表队列
操作 | 最好情况 | 最差情况 |
---|---|---|
enqueue | O(1) | O(1) |
dequeue | O(1) | O(1) |
空间复杂度 | O(n) | O(n) |
环形缓冲区队列
操作 | 最好情况 | 最差情况 |
---|---|---|
enqueue | O(1) | O(1) |
dequeue | O(1) | O(1) |
空间复杂度 | O(n) | O(n) |
双栈队列
1 |
|
操作 | 最好情况 | 最差情况 |
---|---|---|
enqueue | O(1) | O(1) |
dequeue | O(1) | O(1) |
空间复杂度 | O(n) | O(n) |
树
参考文献)
树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
树结构主要用来:
- 代表层级关系
- 管理排序数据
- 促进快速查找操作
术语
节点
树是由节点构成,每个节点封装一些数据并跟踪其子节点。
每个节点负责一个值,并且用数组持有对子节点的引用。
父节点和子节点
每个节点,除了根节点,上面都连接了一个节点,这个节点叫父节点。直接连接在父节点下方的节点叫子节点。
每个子节点只有一个父节点。
根节点
树最顶端的叫根节点
叶节点
没有子节点的节点叫叶节点或者终端节点。
实现
1 | public class TreeNode<T> { |
二叉树
二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。
与普通树不同,普通树的节点个数至少为1,而二叉树的节点个数可以为0;普通树节点的最大分支度没有限制,而二叉树节点的最大分支度为2;普通树的节点无左、右次序之分,而二叉树的节点有左、右次序之分。
二叉树通常作为数据结构应用,典型用法是对节点定义一个标记函数,将一些值与每个节点相关系。这样标记的二叉树就可以实现二叉搜索树和二叉堆,并应用于高效率的搜索和排序。
二叉树的子节点,通常被称为左右子节点。
实现
1 | public class BinaryNode<Element> { |
遍历算法
In-order traversal 中序遍历
从根节点开始:
- 如果当前节点有左子节点,先递归访问这个子节点
- 然后访问当前节点本身
- 如果当前节点有一个右子节点,递归访问这个子节点
1 | extension BinaryNode { |
Pre-order traversal 前序遍历
- 首先访问当前节点自身值
- 如果当前节点有左子节点,递归访问这个子节点
- 如果当前节点有右子节点,递归访问这个子节点
1 | extension BinaryNode { |
Post-order traversal 后序遍历
- 如果当前节点有左子节点,递归访问这个子节点
- 如果当前节点有右子节点,递归访问这个子节点
- 最后访问当前节点自身值
1 | extension BinaryNode { |
二叉搜索树
堆
一个堆是一个完整的二叉树, 也叫做二叉堆, 可以用一个数组构成
堆有两种形式:
- Max heap - 高权重的元素有更大的值
- Min heap - 高权重的元素有更小的值
堆属性
- 在 Max heap 中, 父节点的值必须大于等于子节点的值, 根节点一定是最大值
- 在 Min heap 中, 父节点的值必须小于等于子节点的值, 根节点一定是最小值
- 一个堆一定是一个完整的二叉树, 这意味着, 每一层级必须被填上, 除了最后一级
堆的应用
- 计算一个集合的最小或最大元素
- 堆排
- 生成一个优先队列 -
priority queue
- 生成图标算法, 例如
Prim's
或Dijkstra's
不要混淆这些堆和内存堆
术语堆有时候在计算机科学中被用来只带一个内存池
内存堆是另一个不同的概念
常用操作
Basic Heap
1 | struct Heap<Element: Equatable> { |
Heap Representation
用数组代表堆:
- 左节点下标: 2i + 1
- 右节点下标: 2i + 2
- 父节点下标: (i - 1) / 2
向下筛选去获取左右子节点是 O(log n) 操作, 在数组中, 同样的操作为 O(1)
基本操作:
1 | var isEmpty: Bool { |
Removing from a heap
最基本的移除操作就是移除根节点.
移除最大根节点, 你必须先交换根节点和最后一个元素
注意: max heap, 每个父节点必须必子节点的值要大, 在交换后必须向下筛选. 如果两个子节点的值, 都更大, 那就取最大的那个
1 | /// 移除根节点 |
时间复杂度:
remove()
方法总体时间复杂度是 O(log n) - 数组中交换元素的复杂度是 O(1), 向下筛选的操作复杂度是 O(log n)
插入操作
首先, 在最后一个节点插入值, 然后检查这个完整二叉树还是不是一个最大堆或最小堆, 如果不是, 则要通过和父节点比较, 进行向上筛选
1 | mutating func insert(_ element: Element) { |
时间复杂度: 总共的
insert(_:)
复杂度是 O(log n), 给数组添加元素的操作是 O(1), 向上筛选的复杂度是 O(log n)
O(n2) 算法
Bubble sort
1 | // Bubble sort |
1 | for (NSInteger i = 0; i < mutableArray.count; ++i) { |
Selection sort
1 | // Selection sort |
1 | for (NSInteger current = 0; current < mutableArray.count - 1; current++) { |
Insertion sort
1 | // Insertion sort |
1 | for (NSInteger i = 1; i < mutableArray.count; i++) { |
Merge sort (归并排序)
归并排序是创建在归并操作上的一种有效的排序算法, 效率为 O(nlogn)
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
1 | func mergeSort<Element>(_ array: [Element]) -> [Element] where Element: Comparable { |
Quicksort (快速排序)
1 | // Naive |