基本数据结构(2)——算法导论(12)
1. 引言
这一篇博文主要介绍链表(linked list),指针和对象的实现,以及有根树的表示。
2. 链表(linked list)
我们在上一篇中提过,栈与队列在存储(物理)结构上都可以用数组和链表来实现。数组和链表都是线性存储结构,其中的各元素逻辑上都是按顺序排列的。它们的不同点在于:数组的线性顺序由数组的下标决定;而链表的顺序是由各元素里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法。
如下图所示,双向链表(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。下面是伪代码:
显然,对于一个长度为n的链表,在最坏情况下,搜索的时间为θ(n)。
给定一个已设置好关键字key的元素x,过程INSERT将x插入到链表的最前端。下面是伪代码:
显然,插入的时间与链表的长度无关,为常量θ(1)。
过程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)中,我们用堆来表示一颗完全的二叉树,这里就不一一介绍了。至于哪种方法最优,需要具体情况具体分析。
基本数据结构(2)——算法导论(12)的更多相关文章
- 基本数据结构(1)——算法导论(11)
1. 引言 从这篇博客开始,来介绍一些基本的数据结构知识.本篇及下一篇会介绍几种基本的数据结构:栈.队列.链表和有根树.此外还会介绍由数组构造对象和指针的方法. 这一篇主要介绍栈和队列 ...
- 【python cookbook】【数据结构与算法】12.找出序列中出现次数最多的元素
问题:找出一个元素序列中出现次数最多的元素是什么 解决方案:collections模块中的Counter类正是为此类问题所设计的.它的一个非常方便的most_common()方法直接告诉你答案. # ...
- The Game Of Life – 数据结构与算法的敲门砖
The Game Of Life(生命游戏,又称为细胞自动机)几乎是所有数据结构与算法导论教程前言的一个很经典的程序了.这是一个零玩家游戏,发生在一个平面网格里.每个格子的细胞都有死亡和存活两种状态, ...
- 数据结构和算法(Golang实现)(12)常见数据结构-链表
链表 讲数据结构就离不开讲链表.因为数据结构是用来组织数据的,如何将一个数据关联到另外一个数据呢?链表可以将数据和数据之间关联起来,从一个数据指向另外一个数据. 一.链表 定义: 链表由一个个数据节点 ...
- 数据结构与算法JS实现
行解算法题,没错,就是这么方便. 当然也可以使用 Node.js 环境来执行,具体参考Node.js官方文档即可. 二 对象和面向对象编程 js中5种数据类型,并没有定义更多的数据类型,但是运用j ...
- 算法导论课后习题解答 第一部分 练习1.1-1->1.1-5
很高兴能和大家一起共同学习算法导论这本书.笔者将在业余时间把算法导论后面的题解以博文的形式展现出来希望能得到大家的支持谢谢.如果有可能我会做一些教学视频免费的供大家观看. 练习题选自算法导论中文第三版 ...
- 数据结构和算法(Golang实现)(10)基础知识-算法复杂度主方法
算法复杂度主方法 有时候,我们要评估一个算法的复杂度,但是算法被分散为几个递归的子问题,这样评估起来很难,有一个数学公式可以很快地评估出来. 一.复杂度主方法 主方法,也可以叫主定理.对于那些用分治法 ...
- B树——算法导论(25)
B树 1. 简介 在之前我们学习了红黑树,今天再学习一种树--B树.它与红黑树有许多类似的地方,比如都是平衡搜索树,但它们在功能和结构上却有较大的差别. 从功能上看,B树是为磁盘或其他存储设备设计的, ...
- 堆排序与优先队列——算法导论(7)
1. 预备知识 (1) 基本概念 如图,(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树.树中的每一个结点对应数组中的一个元素.除了最底层外,该树是完全充满的,而且从左向右填充.堆的数组 ...
随机推荐
- UE4新手之编程指南
虚幻引擎4为程序员提供了两套工具集,可共同使用来加速开发的工作流程. 新的游戏类.Slate和Canvas用户接口元素以及编辑器功能可以使用C++语言来编写,并且在使用Visual Studio 或 ...
- 在ASP.NET Core中使用百度在线编辑器UEditor
在ASP.NET Core中使用百度在线编辑器UEditor 0x00 起因 最近需要一个在线编辑器,之前听人说过百度的UEditor不错,去官网下了一个.不过服务端只有ASP.NET版的,如果是为了 ...
- TDD在Unity3D游戏项目开发中的实践
0x00 前言 关于TDD测试驱动开发的文章已经有很多了,但是在游戏开发尤其是使用Unity3D开发游戏时,却听不到特别多关于TDD的声音.那么本文就来简单聊一聊TDD如何在U3D项目中使用以及如何使 ...
- 高性能IO模型浅析
高性能IO模型浅析 服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种: (1)同步阻塞IO(Blocking IO):即传统的IO模型. (2)同步非阻塞IO(Non-blocking ...
- 讓TQ2440也用上設備樹(1)
作者:彭東林 郵箱:pengdonglin137@163.com QQ:405728433 開發板 TQ2440 + 64MB 內存 + 256MB Nand 軟件 Linux: Linux-4.9 ...
- [Nginx笔记]关于线上环境CLOSE_WAIT和TIME_WAIT过高
运维的同学和Team里面的一个同学分别遇到过Nginx在线上环境使用中会遇到TIME_WAIT过高或者CLOSE_WAIT过高的状态 先从原因分析一下为什么,问题就迎刃而解了. 首先是TIME_WAI ...
- AEAI DP V3.7.0 发布,开源综合应用开发平台
1 升级说明 AEAI DP 3.7版本是AEAI DP一个里程碑版本,基于JDK1.7开发,在本版本中新增支持Rest服务开发机制(默认支持WebService服务开发机制),且支持WS服务.RS ...
- 如何手动安装MySql
想安装当然要先有一个MySql的安装包 这里使用的是mysql-5.7.12-winx64 安装包百度云:http://pan.baidu.com/s/1kVAuXuv 密码:hr39 1.要将压缩 ...
- Storm
2016-11-14 22:05:29 有哪些典型的Storm应用案例? 数据处理流:Storm可以用来处理源源不断流进来的消息,处理之后将结果写入到某个存储中去.不像其它的流处理系统,Storm不 ...
- Netty5使用自签证书实现SSL安全连接
这次使用的Netty是最新的5.0 Alpha2版本,下载地址是:http://dl.bintray.com/netty/downloads/netty-5.0.0.Alpha2.tar.bz2,发布 ...