登录
原创

手把手理解数据结构

专栏手把手带你搞定算法
发布于 2020-12-02 阅读 162
  • 算法
  • Swift
原创

数据结构的存储方式

数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。

我们所能看到其他数据结构,比如栈、堆、图、树等等本质都还是在数组或者链表上的特殊操作。

比如说「队列」、「栈」既可以使用链表也可以使用数组实现。用数组实现,就要处理扩容缩容的问题;用链表实现,则需要更多的内存空间存储节点指针。「图」的两种表示方法,邻接表就是链表,邻接矩阵就是二维数组。

可以看到数据结构种类很多,但是底层存储是数组或者链表,二者的优缺点如下:

  • 数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 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)
}

评论区

励志做一条安静的咸鱼,从此走上人生巅峰。

1

0

0

举报