1. 引言html
这一篇博文主要介绍链表(linked list),指针和对象的实现,以及有根树的表示。前端
2. 链表(linked list)node
咱们在上一篇中提过,栈与队列在存储(物理)结构上均可以用数组和链表来实现。数组和链表都是线性存储结构,其中的各元素逻辑上都是按顺序排列的。它们的不一样点在于:数组的线性顺序由数组的下标决定;而链表的顺序是由各元素里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法。算法
以下图所示,双向链表(doubly linked list)L的每一个元素都是一个对象,每一个对象有一个关键字(key)和两个指针:next和prev。对象中还能够包含其余辅助数据(或称为卫星数据)。设x为链表中的某个元素,x.next指向它在链表中的后继元素(也可能没有,此时x.next = NIL,x为链表的最后一个元素,即x的头(head));x.prev则指向它的前驱元素(也可能没有,此时x.prev = NIL,x为链表的第一个元素,即x的尾(tail))。属性L.head指向链表的头,若是为NIL,则链表为空。数组
链表还有多种形式:已排序的(sorted):链表的线性顺序与链表元素中关键字的线性顺序一致;单连接的(singly linked):去掉双链表每一个元素的prev指针);循环链表(circular list):链表头元素的prev指针指向链表尾元素,尾元素的next指针指向头元素。数据结构
咱们能够采用简单的线性搜索方法来搜索链表L中第一个关键字(key)为k的元素,并返回指向该元素的指针。若是没有找到,则返回NIL。下面是伪代码:ide
显然,对于一个长度为n的链表,在最坏状况下,搜索的时间为θ(n)。学习
给定一个已设置好关键字key的元素x,过程INSERT将x插入到链表的最前端。下面是伪代码:this
显然,插入的时间与链表的长度无关,为常量θ(1)。spa
过程DELETE将一个元素x从L中移除。若该过程给出的是一个指向元素x的指针,则可经过修改指针将元素x删除;若是该过程给出的是一个关键字key,则须要先搜索出该元素,而后将其移除。下面是伪代码:
现最坏的状况下,删除操做的时间是θ(n),由于要先搜索出x。
下面给出一种双链表的Java实现:
public class DoublyLinkedList<T> { private Node<T> first; private int size; /** * 将元素插入到链表的最前端 */ public void insert(T t) { Node<T> newNode = new Node<>(null, t, first); if (first != null) { first.prev = newNode; } first = newNode; size++; } /** * 搜索第一次出现待搜索元素的节点 * * @param t * 待搜索元素 * @return 保存该元素的节点(若没找到,返回null) */ public Node<T> search(T t) { Node<T> node = first; while (node != null && node.key != t) { node = node.next; } return node; } /** * 删除链表中第一次出现的待搜索元素的节点 * * @param t */ public void delete(T t) { Node<T> node = search(t); if (node == null) { return; } if (node.prev == null) { // node是first if (node.next != null) { node.next.prev = null; } first = node.next; return; } if (node.prev != null) { node.prev.next = node.next; if (node.next != null) { node.next.prev = node.prev; } } size--; } /** * 根据index获取元素 * * @param index * @return */ public T get(int index) { if (index < 0 || index > size - 1) { throw new IndexOutOfBoundsException(index + ""); } Node<T> node = first; int i = 0; while (node != null && i != index) { node = node.next; i++; } return node == null ? null : node.key; } @Override public String toString() { Node<T> node = first; String result = ""; while (node != null) { String key = node.key == null ? "" : node.key.toString(); result += key + ","; node = node.next; } if (result.endsWith(",")) { result = result.substring(0, result.length() - 1); } return "[" + result + "]"; } public static class Node<T> { T key; Node<T> prev; Node<T> next; public Node(Node<T> prev, T key, Node<T> next) { super(); this.prev = prev; this.key = key; this.next = next; } } public static void main(String[] args) { DoublyLinkedList<Integer> list = new DoublyLinkedList<>(); // 插入 list.insert(1); list.insert(2); list.insert(3); list.insert(4); System.out.println(list); // 搜索 Node<Integer> node = list.search(3); System.out.println(node == null ? "null" : node.key); // 删除 list.delete(1); System.out.println(list); //获取 System.out.println(list.get(2)); System.out.println(list.get(1)); } }
3. 指针和对象的实现
当某种语言不支持指针和对象数据类型时,上面的实现方式是不可行的。这时咱们可考虑用数组和其下标来实现对象和指针。
咱们考虑对对象的每个属性都用一个数组来存放,这样就能够表示一组具备相同属性的的对象。咱们能够用以下图所示的方式来表示上面代码中出现的Node对象。其中数组key,数组prev,数组next分别存放Node的key,prev,next属性。
从上图咱们能够看出,第一个节点在数组下标为7的位置,以后的节点在数组中的下标依次是:5,2,3。
像这样用数组存储的方式与通常使用数组的方式不一样的是,被存储的元素在物理上不是连续的。(暂时还没想到这么作能带来什么好处。但学习算法更重要的是对思惟的扩充。)。
计算机内存的字每每是从整数0到M-1进行编址的,其中M是一个足够大的整数。在许多程序设计语言中,一个对象在计算机内存中占据一组连续的储存单位,指针仅仅是该对象所在的第一个存储单位的地址(就像C中的结构体)。要访问对象内其余储存单元能够在指针上加上一个偏移量。(正如在学习C++时,老师说的,数据类型的本质是固定内存大小的别名)。
一样,咱们能够采用上面的这种策略来实现对象。以下图所示,属性key,next,prev的偏移量分别是0,1,2。
当咱们向一个双向链表表示的动态数组中插入一个元素时,就必须分配一个指向该链表中还没有利用的对象的指针。所以,有必要对链表中还没有利用的对象空间进行管理,使其可以被分配。在某些系统中,有垃圾回收器(garbage collector,GC)负责肯定,回收哪些对象是未使用的。然而许多应用没有GC或者该应用自己很简单,咱们彻底能够本身负责将未使用的对象的存储空间返回给存储管理器。咱们以多数组表示的双向链表为例,探讨同构对象(即有相同属性的对象)的分配与释放的问题。
咱们假设多数组表示法中的各数组的长度为m,且在某一时刻,该动态集合中含有n≤m个元素。这n个对象表示现存于该动态集合中的元素,而余下的m-n个对象是自由的(free),这些自由对象表示的是将要插入该动态集合的元素。
咱们把只有对象保存在一个单链表中,称为自由表(free list)。自由表只使用next数组,该数组只存放链表中的next指针。自由表的头保存在全局变量free中。当有链表L表示的动态集合非空时,自由表可能会和链表L交错,以下图。
自由表相似一个栈:下一个被分配的对象就是最后被释放的那个。咱们能够利用栈的push和pop操做来实现分配和释放过程。伪代码以下:
4. 有根树的表示
上一节介绍的表示链表的方法能够推广到任意同构的数据结构上。在本节中,咱们专门讨论用链式数据结构表示有根树的问题。咱们从最简单的二叉树开始讨论,而后给出针对节点的孩子树任意的有根树的表示方法。
咱们用对象来表示树的节点。与链表相似,假设每一个节点都含有一个关键字key,其他咱们感兴趣的属性包括指向其余节点的指针,它们随树的种类不一样会有所变化。
以下图所示,它展现了在二叉树T中如何利用属性p,left,right存放指向父节点,左孩子,右孩子的指针。属性T.root指向整棵树T的根节点。
二叉树的表示方法能够推广到每一个节点的孩子数至多为常数k的任意类型的树:只须要将left和right属性用child1,child2,…,childk代替。可是当孩子的节点树无限制时,这种方法就失效了,由于不知道预先分配多少个属性(在多数组表示发中就是多少个数组)。此外,即便孩子数k限制在一个大的常数之内,但当多数节点只有少许孩子时,这样作会浪费大量储存空间。
这时咱们能够用一种叫作左孩子右兄弟表示法(left-child,right-sibling representation),以下图所示。对任意n个节点的有根树,它只须要O(n)的存储空间。与前面相似,每一个节点都包含一个父节点指针p,且T.root指向树T的根节点。然而,每一个节点中不是包含指向每一个孩子的指针,而是只有两个指针:x.left-child指向节点x最左边的孩子节点。x.right-sibling指向节点x右侧相邻的兄弟节点。
事实上,咱们还能够用许多其余的方法来表示有根树,例如在前面介绍的堆排序与优先队列——算法导论(7)中,咱们用堆来表示一颗彻底的二叉树,这里就不一一介绍了。至于哪一种方法最优,须要具体状况具体分析。