1. 引言

    这一篇博文主要介绍链表(linked list)指针和对象的实现,以及有根树的表示

2. 链表(linked list)

(1) 链表介绍

     我们在上一篇中提过,栈与队列在存储(物理)结构上都可以用数组和链表来实现。数组和链表都是线性存储结构,其中的各元素逻辑上都是按顺序排列的。它们的不同点在于:数组的线性顺序由数组的下标决定;而链表的顺序是由各元素里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法。

    如下图所示,双向链表(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指针指向头元素。

(2) 链表的搜索

    我们可以采用简单的线性搜索方法来搜索链表L中第一个关键字(key)为k的元素,并返回指向该元素的指针。如果没有找到,则返回NIL。下面是伪代码:

    显然,对于一个长度为n的链表,在最坏情况下,搜索的时间为θ(n)。

(3) 链表的插入

    给定一个已设置好关键字key的元素x,过程INSERT将x插入到链表的最前端。下面是伪代码:

    显然,插入的时间与链表的长度无关,为常量θ(1)。

(4) 链表的删除

    过程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. 指针和对象的实现

    当某种语言不支持指针和对象数据类型时,上面的实现方式是不可行的。这时我们可考虑用数组和其下标来实现对象和指针。

(1) 对象的多数组表示

    我们考虑对对象的每一个属性都用一个数组来存放,这样就可以表示一组具有相同属性的的对象。我们可以用如下图所示的方式来表示上面代码中出现的Node对象。其中数组key,数组prev,数组next分别存放Node的key,prev,next属性。

    从上图我们可以看出,第一个节点在数组下标为7的位置,之后的节点在数组中的下标依次是:5,2,3。

    像这样用数组存储的方式与一般使用数组的方式不同的是,被存储的元素在物理上不是连续的。(暂时还没想到这么做能带来什么好处。但学习算法更重要的是对思维的扩充。)。

(2) 对象的单数组表示

    计算机内存的字往往是从整数0到M-1进行编址的,其中M是一个足够大的整数。在许多程序设计语言中,一个对象在计算机内存中占据一组连续的储存单位,指针仅仅是该对象所在的第一个存储单位的地址(就像C中的结构体)。要访问对象内其他储存单元可以在指针上加上一个偏移量。(正如在学习C++时,老师说的,数据类型的本质是固定内存大小的别名)。

    同样,我们可以采用上面的这种策略来实现对象。如下图所示,属性key,next,prev的偏移量分别是0,1,2。

(3) 对象的分配与释放

    当我们向一个双向链表表示的动态数组中插入一个元素时,就必须分配一个指向该链表中尚未利用的对象的指针。因此,有必要对链表中尚未利用的对象空间进行管理,使其能够被分配。在某些系统中,有垃圾回收器(garbage collector,GC)负责确定,回收哪些对象是未使用的。然而许多应用没有GC或者该应用本身很简单,我们完全可以自己负责将未使用的对象的存储空间返回给存储管理器。我们以多数组表示的双向链表为例,探讨同构对象(即有相同属性的对象)的分配与释放的问题。

    我们假设多数组表示法中的各数组的长度为m,且在某一时刻,该动态集合中含有n≤m个元素。这n个对象表示现存于该动态集合中的元素,而余下的m-n个对象是自由的(free),这些自由对象表示的是将要插入该动态集合的元素。

    我们把只有对象保存在一个单链表中,称为自由表(free list)。自由表只使用next数组,该数组只存放链表中的next指针。自由表的头保存在全局变量free中。当有链表L表示的动态集合非空时,自由表可能会和链表L交错,如下图。

    自由表类似一个栈:下一个被分配的对象就是最后被释放的那个。我们可以利用栈的push和pop操作来实现分配和释放过程。伪代码如下:

4. 有根树的表示

1. 介绍

    上一节介绍的表示链表的方法可以推广到任意同构的数据结构上。在本节中,我们专门讨论用链式数据结构表示有根树的问题。我们从最简单的二叉树开始讨论,然后给出针对节点的孩子树任意的有根树的表示方法。

    我们用对象来表示树的节点。与链表类似,假设每个节点都含有一个关键字key,其余我们感兴趣的属性包括指向其他节点的指针,它们随树的种类不同会有所变化。

2. 二叉树

    如下图所示,它展示了在二叉树T中如何利用属性p,left,right存放指向父节点,左孩子,右孩子的指针。属性T.root指向整棵树T的根节点。

3. 分支无限制的有根树

    二叉树的表示方法可以推广到每个节点的孩子数至多为常数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右侧相邻的兄弟节点。

4. 其他

    事实上,我们还可以用许多其他的方法来表示有根树,例如在前面介绍的堆排序与优先队列——算法导论(7)中,我们用堆来表示一颗完全的二叉树,这里就不一一介绍了。至于哪种方法最优,需要具体情况具体分析。

基本数据结构(2)——算法导论(12)的更多相关文章

  1. 基本数据结构(1)——算法导论(11)

    1. 引言     从这篇博客开始,来介绍一些基本的数据结构知识.本篇及下一篇会介绍几种基本的数据结构:栈.队列.链表和有根树.此外还会介绍由数组构造对象和指针的方法.     这一篇主要介绍栈和队列 ...

  2. 【python cookbook】【数据结构与算法】12.找出序列中出现次数最多的元素

    问题:找出一个元素序列中出现次数最多的元素是什么 解决方案:collections模块中的Counter类正是为此类问题所设计的.它的一个非常方便的most_common()方法直接告诉你答案. # ...

  3. The Game Of Life – 数据结构与算法的敲门砖

    The Game Of Life(生命游戏,又称为细胞自动机)几乎是所有数据结构与算法导论教程前言的一个很经典的程序了.这是一个零玩家游戏,发生在一个平面网格里.每个格子的细胞都有死亡和存活两种状态, ...

  4. 数据结构和算法(Golang实现)(12)常见数据结构-链表

    链表 讲数据结构就离不开讲链表.因为数据结构是用来组织数据的,如何将一个数据关联到另外一个数据呢?链表可以将数据和数据之间关联起来,从一个数据指向另外一个数据. 一.链表 定义: 链表由一个个数据节点 ...

  5. 数据结构与算法JS实现

      行解算法题,没错,就是这么方便. 当然也可以使用 Node.js 环境来执行,具体参考Node.js官方文档即可. 二 对象和面向对象编程 js中5种数据类型,并没有定义更多的数据类型,但是运用j ...

  6. 算法导论课后习题解答 第一部分 练习1.1-1->1.1-5

    很高兴能和大家一起共同学习算法导论这本书.笔者将在业余时间把算法导论后面的题解以博文的形式展现出来希望能得到大家的支持谢谢.如果有可能我会做一些教学视频免费的供大家观看. 练习题选自算法导论中文第三版 ...

  7. 数据结构和算法(Golang实现)(10)基础知识-算法复杂度主方法

    算法复杂度主方法 有时候,我们要评估一个算法的复杂度,但是算法被分散为几个递归的子问题,这样评估起来很难,有一个数学公式可以很快地评估出来. 一.复杂度主方法 主方法,也可以叫主定理.对于那些用分治法 ...

  8. B树——算法导论(25)

    B树 1. 简介 在之前我们学习了红黑树,今天再学习一种树--B树.它与红黑树有许多类似的地方,比如都是平衡搜索树,但它们在功能和结构上却有较大的差别. 从功能上看,B树是为磁盘或其他存储设备设计的, ...

  9. 堆排序与优先队列——算法导论(7)

    1. 预备知识 (1) 基本概念     如图,(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树.树中的每一个结点对应数组中的一个元素.除了最底层外,该树是完全充满的,而且从左向右填充.堆的数组 ...

随机推荐

  1. javascript中的Array对象 —— 数组的合并、转换、迭代、排序、堆栈

    Array 是javascript中经常用到的数据类型.javascript 的数组其他语言中数组的最大的区别是其每个数组项都可以保存任何类型的数据.本文主要讨论javascript中数组的声明.转换 ...

  2. CRC、反码求和校验 原理分析

    3月份开始从客户端转后台,算是幸运的进入全栈工程师的修炼阶段.这段时间一边是老项目的客户端加服务器两边的维护和交接,一边是新项目加加加班赶工,期间最长经历了连续工作三天只睡了四五个小时的煎熬,人生也算 ...

  3. 07.LoT.UI 前后台通用框架分解系列之——轻巧的文本编辑器

    LoT.UI汇总:http://www.cnblogs.com/dunitian/p/4822808.html#lotui 上次说的是强大的百度编辑器 http://www.cnblogs.com/d ...

  4. Content Security Policy 入门教程

    阮一峰文章:Content Security Policy 入门教程

  5. 重新认识了下Entity Framework

    什么是Entity Framework Entity Framework是一个对象关系映射O/RM框架. Entity Framework让开发者可以像操作领域对象(domain-specific o ...

  6. 开发者的利器:Docker 理解与使用

    困扰写代码的机器难免会被我们安装上各种各样的开发工具.语言运行环境和引用库等一大堆的东西,长久以来不仅机器乱七八糟,而且有些相同的软件还有可能会安装不同的版本,这样又会导致一个项目正常运行了,却不小心 ...

  7. C#调用C++代码遇到的问题总结

    最近在开发服务后台的时候,使用c#调用了多个c++编写的dll,期间遇到了一系列的问题,经过一番努力最后都一一解决了,在此做个总结,方便以后参考,毕竟这些问题也都是很常见的,主要有以下问题: 类型对照 ...

  8. SOLID 设计原则

    SOLID 原则基本概念: 程序设计领域, SOLID (单一功能.开闭原则.里氏替换.接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象 ...

  9. Spring注解

    AccountController .java Java代码   1.        /** 2.         * 2010-1-23 3.         */ 4.        packag ...

  10. Atitit 软件工程概览attilax总结

    Atitit 软件工程概览attilax总结 1.1. .2 软件工程的发展 进一步地,结合人类发展史和计算机世界演化史来考察软件工程的发展史. 表2 软件工程过程模型 表2将软件工程的主要过程模型做 ...