数据结构的存储方式
数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。
我们所能看到其他数据结构,比如栈、堆、图、树等等本质都还是在数组或者链表上的特殊操作。
比如说「队列」、「栈」既可以使用链表也可以使用数组实现。用数组实现,就要处理扩容缩容的问题;用链表实现,则需要更多的内存空间存储节点指针。「图」的两种表示方法,邻接表就是链表,邻接矩阵就是二维数组。
可以看到数据结构种类很多,但是底层存储是数组或者链表,二者的优缺点如下:
-
数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 O(N);而且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度 O(N)。
-
链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。
数据结构的操作方式
对于任何数据结构,其基本操作无非遍历 + 访问。概括一下,增删改查。
各种数据结构的遍历 + 访问无非两种形式:线性的和非线性的。线性就是 for/while 迭代为代表,非线性就是递归为代表。
- 数组遍历框架,典型的线性迭代结构:
func traverse(arr: [Int]) {
for i in 0..<arr.count {
// 迭代访问 arr[i]
}
}
- 链表遍历框架,兼具迭代和递归结构:
/* 基本的单链表节点 */
public class ListNode {
public var val: Int
public var next: ListNode?
public init(_ val: Int) {
self.val = val
self.next = nil
}
}
func reverse(_ head: ListNode?) {
var p = head
while p != nil {
p = p?.next
// 迭代访问 p.val
}
}
func reverse(_ head: ListNode?) {
// 递归访问 head.val
traverse(head?.next);
}
- 二叉树遍历框架,典型的非线性递归遍历结构:
public class TreeNode {
public var val: Int
public var left: TreeNode?
public var right: TreeNode?
public init(_ val: Int) {
self.val = val
self.left = nil
self.right = nil
}
}
func traverse(_ root: TreeNode?) {
traverse(root?.left)
traverse(root?.right)
}