- 2.1 线性表的定义和特点
- 2.2 案例引入
- 2.3 线程表的类型定义
- 2.4 线性表的顺序表示和实现
- 2.4.1 线性表的顺序存储表示
- 2.4.2 线性表的结构类型定义
- 2.4.3 顺序表基本操作的实现
- 2.4.4 顺序表总结
- 2.5 线性表的链式表示和实现
- 2.5.1 线性表的链式存储表示
- 2.5.2 单链表的实现
- (1)单链表的基本操作
- (2)单链表的取值
- (3)单链表的查找
- (4)单链表的插入
- (5)单链表的删除
- (6)单链表的建立
- 2.5.3 循环链表的实现
- 2.5.4 双向链表的实现
- (1)双向链表的插入
- (2)双向链表的删除
- 2.6 顺序表和链表的比较
- 2.7 线性表的应用
- 2.7.1 线性表的合并
- 2.7.2 有序表的合并
- (1)顺序表实现
- (2)链表实现
2.1 线性表的定义和特点
线性表示具有相同特性的数据元素的一个有限序列
线性表的例子:
同一线性表中的元素必定具有相同的特性,数据元素间的关系是线性关系。
2.2 案例引入
【案例2.1】
一元多项式的运算:实现俩个多项式加、减、乘运算
我们可以将每个项的系数存到线程表,指数可以通过系数的下标隐含的表示。比如以下:
以上是理想情况下,但如果像以下这种稀疏多项式:
如果还是按照以上的方式存储,那么我们需要 20001个存储空间,但实际上却只存储了三个数据,非常浪费空间:
因此我们可以利用一个二维数组,分别用来记录 系数 和 指数,其他没有用的数据不要记录:
代码表示:
int[][] table = {{1,0},{3,10000},{2,20000}};
【案例2.2】
稀疏多项式的运算
- 创建一个新数组c
- 分别从头遍历线性表A和线性表B
- 指数相同:将系数相加,若不为0,则将相加后的放到c中
- 指数不同:将指数小的放到c中
- 当其中一个遍历完毕后,剩余的依次放到 c 中。
问题:
这个数组C初始化多大合适呢? 如果初始化为俩个线性表大小的和,但实际可能会由于指数相等,用不了这么多的空间,因此会浪费空间。太小又会不够用。
因此对于顺序存储结构来说,他的缺点:
- 空间分配不够灵活
- 运算空间复杂度高【需要借助数组c】
对于以上问题,我们可以使用链式存储结构解决。
操作步骤和上面一样,区别就是不需要额外的存储空间,并且可以动态的分配存储空间:
【案例2.3】
图书信息管理系统
我们可以将左边的这张图书表看做为线程表,每本书的信息可以看做是一个元素。
与前俩个案例相比,该数据元素不再是一个简单的数据类型,而是一个复杂的对象,每一个对象包含:书号、书名、定价
三个属性信息。
总结
-
线性表中数据元素的类型可以为作
简单类型
也可以为复杂类型
-
许多实际应用问题所涉的基本操作有很大相似性,不应为每个具体应用单独编写一个程序
-
从具体应用中抽象出共性的
逻辑结构和基本操作
,然后实现其存储结构和基本操作
2.3 线程表的类型定义
线性表的定义:
ADT List{
数据对象: D={ai | ai ∈ ElemSet, i=1, 2, …, n, n>=0}
数据关系: R=(<ai-1,ai> | ai-1,ai ∈D, i=2, …, n}
基本操作:
InitList (&L)
操作结果:构造一个空的线性表L。
DestroyList(&L)
初始条件:线性表L已存在。
操作结果:销毁线性表L。
ClearList (&L)
初始条件:线性表L已存在。
操作结果:将L重置为空表。
ListEmpty(L)
初始条件:线性表L已存在。
操作结果:若L为空表, 则返回true, 否则返回false。
ListLength(L)
初始条件:线性表L已存在。
操作结果:返回L中数据元素个数。
GetElem(L,i,&e)
初始条件:线性表L巳存在,且1<=i<=ListLength(L)。
操作结果:用e返回L中第i个数据元素的值。
LocateElem(L,e)
初始条件:线性表L已存在。
操作结果:返回L中第1个 值与e相同的元素在 L中的位置 。若这样的数据元素不存在 , 则返回值为0。
PriorElem(L,cur_e,&pre_e)
初始条件:线性表L已存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义。
NextElem(L,cur_e,&next_e)
初始条件:线性表L已存在。
操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败,next_e无定义。
Listinsert(&L,i,e)
初始条件:线性表L已存在,且1<=i<=ListLength(L)+1
操作结果:在 L中第1个位置之前插入新的数据元素 e, L的长度加1。
ListDelete(&L,i)
初始条件:线性表L已存在且非空 ,且1<=i<=ListLength(L)
操作结果:删除L的第1个数据元素,L的长度减1。
TraverseList(L)
初始条件:线性表L已存在。
操作结果:对线性表L进行遍历,在遍历过程中对 L的每个结点访问一次。
) ADT List
以上所提及的运算是逻辑结构上定义的运算。只要给出这些运算的功能是 “做什么”,至于"如何做"等实现细节,只有待确定了存储结构之后才考虑。
实现参考:2.4、2.5
2.4 线性表的顺序表示和实现
2.4.1 线性表的顺序存储表示
线性表俩种基本的存储结构:顺序存储、链式存储。
顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构中。
简单来说,逻辑上相邻的数据元素,在物理上也相邻
【例子】
因此我们可以得知,顺序存储的最大特点就是 它会占用一片连续的存储空间(地址连续,依次存放),知道某一个元素的位置就可以计算出其他元素的位置(随机存取)
。
顺序表中元素存储位置的计算
如果 a1~an每个元素占用8个存储单元, ai 存储的位置是 2000 单元,则 ai+1的存储位置是?
ai 的存储位置是:2000 ~ 2007, ai+1 的存储位置起始点为:2008
假设线性表的每个元素需占 l 个存储单元,则第 i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:
由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到:
2.4.2 线性表的结构类型定义
在线性表定义中的删除操作是可以动态改变线性表的长度的,但是数组是固定长度,无法动态定义。因此我们可以用一个变量表示线性表的长度
。
知道了存储数据元素的存储结构,那么该如何定义线性表的结构类型(Java实现):
【多项式结构类型】
// 多项式非零项的定义
class Ele{
private Float p; // 系数
private Integer e; // 指数
}
// 结构类型定义
class StructType {
Ele[] elem; // 存储多项式元素
int length; // 线性表长度
int maxSize; // 数组初始化长度
}
【图书管理系统】
// 结构类型定义
class StructType {
Book[] elem; // 存储图书信息元素
int length; // 线性表长度
int maxSize; // 数组初始化长度
}
// 图书信息对象
class Book {
String no; // 图书编号
String name; // 图书名
Float price; // 图书价格
}
2.4.3 顺序表基本操作的实现
使用数组实现顺序表,它的起始下标是从0开始的,因此与逻辑位序相差1
顺序表示意图
用Java语言来解释的话,SqList 是一个类,L 是一个对象,也是一个顺序表,用来存储线性表的元素。
绿色部分存储的是线性表的元素,蓝色部分是线性表元素的个数。绿色部分+蓝色部分就是顺序表L。
下面用Java实现基本操作操作:
在 Java中,由于java的垃圾回收机制会自动释放不在使用的内存空间,无需我们自己操作。
// 定义结构类型
class StructTypeImplementation {
// private
Object[] elem; // 存储线性表元素,自定义所需要的类型。
int maxSize = 100; // 数组初始化大小
int length; // 线性表长度
// 初始化
public StructTypeImplementation(int maxSize) {
this.elem = new Object[maxSize];
this.maxSize = maxSize;
this.length = 0;
}
// 获取线性表长度
public int getLength(){
return length;
}
// 判断是否为空
public boolean isEmpty(){
return length == 0;
}
// 获取第i个位置的元素【随机存取】
// 这也是顺序表最大的特点
public Object getElem(int i){
// 如果获取的位置小于1或者大于线性表的长度,那么这个参数是不合法的
if (i < 1 || i > length) throw new IllegalArgumentException("参数错误");
return elem[i-1];
}
// 查找某个元素所在的位置。 -1表示没有找到
public int LocateElem(Object keyword){
for (int i = 0; i < elem.length; i++) {
if (elem[i] == keyword) return i+1;
}
return -1;
}
}
以上基本操作非常简单,就不一一讲述…
顺序表的插入算法
线性表的插入运算是指在表的第i (i <= i <= n+1)
个位置上,插入一个新结点e,使长度为n的线性表(a1,…, ai -1,ai,…, an) 变成长度为n+1 的线性表(a1,…, ai -1, e, ai,…, an)
假如想要将 f 插入第3个位置上(下标为2),需要将插入位置及之后的元素往后移,也就是下标范围: 【 2~n-1】
的元素。【n就是线性表的长度,对应代码中length变量】
将 e 移动下标为5 的位置上,d移动到下标为 4 的位置上,c 移动到下标为 3 的位置上。留出下标为 2 的位置,将 f 插入进去。
最后将线性表长度 n+ 1
总结起来就是,假设想要往第 i 个位置上插入元素,就要将下标在 [ i-1 ,n- 1 ]这个范围内的元素往后移。空出第 i 个位置
算法思想:
- 判断插入位置是否合法
- 合法位置:1 ~ n+1
- 判断顺序表是否已满,若已满返回ERROR
- 将第 n 至 i 位的元素依次向后移动一个位置,空出第 i 个位置
- 将要插入的元素 放入第 i 个位置,线性表长度+1
注意:位置和下标相差1,比如:第3个位置的元素它的下标为2
Java实现
// 将元素 e 插入 index上
public boolean insertElem(Object e, int index) {
// 1、判断位置是否合法。 1 <= index <= length + 1
if (index < 1 || index > getLength() + 1) return false;
// 2、判断数组是否已经满了
if (length == maxSize) return false;
// 移动 index-1 ~ length-1 的元素
for (int i = length-1; i >= index -1; i--) {
// 往后移动一位
elem[i+1] = elem[i];
}
elem[index-1] = e;
// 线性表长度+1
++length;
return true;
}
时间复杂度分析
代码中执行次数最多的语句时 for 循环体,消耗的时间主要在移动元素上。移动的次数取决于插入的位置:
- 若插入在尾结点之后,则根本无需移动 (特别快)
- 若插入在首结点之前,则表中元素全部后移 (特别慢)
- 若要考虑在各种位置插入 (共n+1种可能)的平均移动次数,该如何计算?
平均时间复杂度为:O(n)
顺序表的删除
线性表的删除运算是指将表的第 i (1 <= i<=n)个结点删除使长度为n 的线性表(a1, …, ai-1, ai , ai +1 …, an)变成长度为n-1的线性表 (a1,… ai-1 ai+1…, an)
由于使用数组模拟线性表,无法直接删除一个元素,只能将删除元素后边的元素,往前移进行覆盖。
以下图为例,想要删除第3个位置的元素,需要将 第4、5个位置上元素的往前移。
算法思想:
- 检查删除位置 i 是否合法,范围应该在: 1<= i <= n
- 如果需要返回删除的元素,将元素赋值给一个变量。如果不需要此步骤可省略
- 将第 [i + 1, n] 的元素往前移
- 顺序表长度n-1
注意:位置和下标相差1,比如:第3个位置的元素它的下标为2
Java代码实现
// 删除index位置的元素,并返回该位置的元素
public Object deleteElem(int index){
// 1、检查删除位置是否合法 1<=index<=length
if (index < 1 || index > length) return false;
// 删除位置的元素,提前赋值给res
Object res = elem[index-1];
// 2、将[index,length)位置的元素往前移
for (int i = index; i < length; i++) {
elem[i-1] = elem[i];
}
--length;
return res;
}
2.4.4 顺序表总结
1、利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致
。
2、在访问线性表时,可以快速地计算出任何一个数据元素的存储地址,一因此可以粗略的认为,访问每个元素所花时间相等
3、这种存取元素的方法称为随机存取
-
优点:
- 存储密度大 (结点本身所占存储量/结点结构所占存储量)
- 可以随机存取表中任一元素
-
缺点:
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式数据元素的个数不能自由扩充
链式存储解决了以上的缺点~!
2.5 线性表的链式表示和实现
2.5.1 线性表的链式存储表示
结点在存储器中的位置是任意
的,即逻辑上相邻的数据元素在物理上不一定相邻,线性表的链式表示又称为非顺序映像
或链式映像
链表的存储熟顺序是任意的,每一个结点中不仅包括数据本身,还包括指向下一结点的指针,这样就可以将所有数据串联起来,形成一个链。
^ 表示指针域为NULL
各结点由俩个域组成:
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置
链式存储相关术语
1、结点: 数据元素的存储映像。由数据域
和指针域
两部分组成
2、链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构
单链表、双链表、循环链表
结点只有一个指针域的链表,称为单链表或线性链表
结点有两个指针域的链表,称为双链表
首尾相接的链表称为循环链表。单链表、双向链表的尾结点的指针域为NULL,而循环链表尾结点的指针域是头结点的地址。
头指针、头结点、首元结点
头指针: 是指向链表中第一个结点的指针,头指针是一个链表中必须存在的,指明了链表的存储地址。头指针就是链表。
首元结点:是指链表中存储第一个数据元素a的结点
头结点:是在链表的首元结点之前附设的一个结点
链式存储俩种变现形式
不带头结点:
带头结点:
如何表示空表
不带头结点:头指针为空时表示空表
带头结点: 头结点的指针域为空
带头结点有什么好处
1、便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
2、便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针因此空表和非空表的处理也就统一了
假设 L 为单链表的头指针,它应该指向首元结点,则当单链表为长度 n 为 0 的空表时, L 指针为空(判定空表的条件可记为:L== NULL)。
增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如图所示 的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为: L ->next== NULL)
头结点的数据域内装的是什么
头结点的数据域可以为空,也可存放线性表长康等附加信息,但此结点不能计入链表长度值
。
链式存储结构特点
(1)结点在存储器中的位置是任意的即逻辑上相邻的数据元素在物理上不一定相邻
(2) 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
这种存取元素的方法被称为顺序存取法
顺序表:随机存取,顺序存储
链式表:随机存储,顺序存取
不要搞混!!!!
2.5.2 单链表的实现
(1)单链表的基本操作
单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。若头指针名是L,则把链表称为表L
单链表的初始化(带头结点的单链表)
即构造一个如图的空表
// 单向链表的一些基本操作
class LinkedList {
Node dummyHead; // 头结点
// 初始化链表
public LinkedList() {
// 头结点,数据域可以不存储值或者存储任意值
this.dummyHead = new Node(-1);
}
}
// 定义结点
class Node {
Object data; // 数据域
Node next; // 指针域
// 初始化一个节点
public Node(Object data) {
this.data = data;
this.next = null;
}
}
在 Java中存在自动垃圾回收机制,不需要使用链表时,只需要将链表设置为 NULL 即可。
清空单链表
Java中清空单链表只需要将头结点设置为null,与其他结点不可达,垃圾回收机制会自动回收其他结点
public void clear(){
dummyHead = null;
}
求链表的表长
从首元结点开始,依次遍历所有结点
- 用一个引用p【C中的指针类似于Java中的引用】指向链表的首元结点
- 当p的next域不为空,说明有下一个结点,i++ 并且移动p指向下一个结点【p=p.next】
- 当p的next域为空,说明p指向了最后一个结点,结束循环。
代码实现:
// 获取链表的长度
public int length() {
// 记录链表长度
int i = 0;
// p指向首元结点
Node p = dummyHead.next;
while (p != null) {
i++;
// 移动p
p = p.next;
}
return i;
}
(2)单链表的取值
获取单链表中第 i 个元素的内容
- 初始化变量: j =1 :表示遍历到第j结点。p 指向第一个首元结点。
- 循环遍历:从首元结点开始循环遍历,循环终止条件:p = null
- 比较 j 和 i 的值,相等则返回 p 指向结点的 data 域
- 不相等,则移动p 并且将 j++,继续判断 j 和 i 的值
特殊情况:在循环之前,必须要校验i 的合法性,也就是说想要查找的元素必须在: [j, length]
这个区间之内。length为链表的长度。
通过以上分析,其实也可以得知,链表不是随机存取结构,想要获取某个结点必须从首元结点开始遍历。
代码实现:
public Object getELement(int i){
// 初始化
int j = 1;
Node p = dummyHead.next;
// 循环遍历
while(p != null) {
if (j == i) {
return p.data;
}
// 移动p指向下一个结点
p = p.next;
j++;
}
// 可以在这里直接抛出一个异常, 代替i的校验
// 因为范围内的i必然会找到
throw new IndexOutOfBoundsException("Index i is out of bounds");
}
(3)单链表的查找
获取与 e 值相等的结点的位置
- 初始化变量: j =1 :表示遍历到第j结点。p 指向第一个首元结点。
- 循环遍历:从首元结点开始循环遍历,循环终止条件:p= null
- 比较p.data 和 e ,相等则返回 j
- 不相等,则移动p 并且将 j++,继续判断 data域和e的值
代码实现:
public int locateEle(Object e){
// 初始化
int j = 1;
Node p = dummyHead.next;
while(p != null) {
if (p.data == e) {
return j;
}
// 移动p指向下一个结点
p = p.next;
j++;
}
// 没有找到
return -1;
}
时间复杂度:
因线性表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)
(4)单链表的插入
在第i个结点前插入值为e的新结点
步骤:
- 首先找到 ai-1 的存储位置p
- 生成一个数据域为e的新结点S
- 插入新结点
- 1、新结点的指针域指向结点 ai , s.next = p.next
- 2、结点 a i-1 的指针域指向新结点, p.next = s
特殊情况: i 的范围 [j, length+1]
注意: 2 和 1 不能互换,如果先执行 2 ,ai 的地址会被 p.next = s 覆盖
代码实现:
public void insertELe(int i,Object e) {
// 初始化,p指向头结点
int j = 0;
Node p = dummyHead;
while(p != null) {
// 找到插入位置的前一个结点
if (j==i-1){
// 创建新结点
Node node = new Node(e);
node.next = p.next;
p.next = node;
return;
}
p = p.next;
j++;
}
// i 参数错误
if (j < i-1 || i > length()+1) {
throw new IllegalArgumentException("Index i is out of bounds");
}
}
(5)单链表的删除
删除第i个结点
【步骤】
- 仍然是先找到 ai-1 的存储位置p,如果有需要则保存 ai 的值
- 将 ai-1 的 指针域指向 ai+1 【p.next = p.next.next】
- 将 ai 结点置空,释放 空间
代码实现:
public boolean delEle(int i) {
// 初始化:指向第一个结点
int j = 0;
Node p = dummyHead;
while(p != null) {
// 找到删除结点的前一个结点
if (j == i -1) {
// 删除
p.next = p.next.next;
p.next.next = null;
return true;
}
// 后移
p = p.next;
j++;
}
return false;
}
删除和插入时间复杂度:
因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)
(6)单链表的建立
头插法
将元素插入在链表头部,也叫前插法。
从最后一个结点开始,依次将各结点插入到链表的前端
【步骤】
- 将新结点的 指针域 ,指向头结点 指针域
- 将头结点的 指针域 指向新结点
代码实现:
public void createList_head(Node node){
node.next = dummyHead.next;
dummyHead.next = node;
}
尾插法
将新结点插入链表的尾部,也叫后插法
- 使用一个变量p指向头结点,然后循环将p指向最后一个结点
- 将p结点的指针域指向新结点
public void createList_tail(Node node) {
Node p = dummyHead;
// 指向最后一个结点
while(p.next != null) {
p = p.next;
}
p.next = node;
}
时间复杂度:O(n) , 需要将 p 移动到链表的尾部
2.5.3 循环链表的实现
循环链表:是一种头尾相接的链表。(即:表中最后一个结点的指针域指向头结点,形成一个环)
优点: 从表中任一结点出发均可找到表中的其他结点
注意:
在单链表中判断非空时的条件为 p.next 是否为空,而在循环链表中则要判断 p.next 是否等于头结点。
单链表的循环条件判断 p!=null 或者 p.next != null
循环链表的循环条件判断 p!= head 或者 p.next != head
带尾指针的循环链表
【举例】
带尾指针循环链表的合并(将Tb合并在Ta之后)
分析有哪些操作?
- 将Ta的尾结点指向Tb的首元结点
- 将Tb的尾结点指向Ta的头结点
- 将Ta的尾结点设置成Tb的尾结点
- 将Tb的头结点置空
代码实现:
public class CircularLinkedList {
public static void main(String[] args) {
CircularList ta = new CircularList();
ta.insertTail(new CircularNode(1));
ta.insertTail(new CircularNode(2));
ta.insertTail(new CircularNode(3));
// ta.print();
CircularList tb = new CircularList();
tb.insertTail(new CircularNode(4));
tb.insertTail(new CircularNode(5));
tb.insertTail(new CircularNode(6));
// tb.print();
ta.merge(tb);
ta.print();
}
}
// 带尾指针循环链表
class CircularList {
CircularNode dummyHead; // 头结点
CircularNode tail; // 尾结点
/**
* 初始化链表
* */
public CircularList() {
// 头结点,数据域可以不存储值或者存储任意值
this.dummyHead = new CircularNode(-1);
// 初始状态,尾结点也是 头结点,形成一个闭环
this.tail = this.dummyHead;
}
/*其他操作省略...*/
// 尾插法
public void insertTail(CircularNode node){
tail.next = node;
node.next = dummyHead;
tail = node; // 更新尾结点
}
// 打印链表
public void print(){
CircularNode current = dummyHead.next;
while(current != dummyHead) {
// !!! 这里如果打印current会报错栈溢出
// 原因是: 打印current会调用CircularNode中的toString,而toSting中的next又会调用CircularNode中的toString,无限循环
System.out.print(current.data + "\t");
current = current.next;
}
}
// 合并俩个循环链表(不考虑空表的情况)
public void merge(CircularList tb) {
// 将ta的尾节点指向tb的首元结点
tail.next = tb.dummyHead.next;
// 将 tb 的尾结点指向 ta 的头结点
tb.tail.next = dummyHead;
// 将ta的尾结点设置成tb的尾结点
tail = tb.tail;
// 将tb头结点置空
tb.dummyHead = null;
}
}
class CircularNode {
Object data; // 数据域
CircularNode next; // 指针域
// 初始化一个节点
public CircularNode(Object data) {
this.data = data;
this.next = null;
}
@Override
public String toString() {
return "CircularNode{" +
"data=" + data +
", next=" + next +
'}';
}
}
2.5.4 双向链表的实现
以上讨论的链式存储结构的结点中 只有一个指示 直接后继的指针域, 由此, 从某个结点 出发 只能顺指针向后寻查其他结点。 若要寻查结点的直接前驱,则必须从表头指针出发。 换句话说, 在单链表中,查找直接后继结点的执行时间为 0(1), 而查找直接前驱的执行时间为O(n)。
为克服 单链表这种单向性的缺点,可利用双向链表 (Double Linked List)。
双向链表: 在单链表的每个结点里在增加一个指向其直接前驱的指针域 prior,这样链表中就形成了有俩个方向不同的链,故称为双向链表
双向链表的结构:
prior 指向前一个结点
next 指向后一个结点
空表时,prior 和 next域都为空
双向链表的定义:
class DNode {
Object data;
DNode next; // 指向后继结点指针域
DNode prior; // 指向前驱结点的指针域
// 初始化一个结点
public DNode(Object data) {
this.data = data;
this.next = null;
this.prior = null;
}
@Override
public String toString() {
return "DNode{" +
"data=" + data +
", next=" + next +
", prior=" + prior +
'}';
}
}
双向循环链表
和单链的循环表类似,双向链表也可以有循环表。
- 让头结点的前驱指针指向链表的最后一个结点
- 让最后一个结点的后继指针指向头结点
双向链表的结构的对称性
假设指针p指向某一个结点
在双向链表中有些操作(如: istLenth、GetElem等),因仅涉及个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为 On。
(1)双向链表的插入
在第i个位置上插入新结点
与单链表相比,除了要处理next域,还要处理 prior 域。
单链表插入需要找到 i-1 个结点,而双向链表直接找到第 i 个结点,通过prior 域找到 i-1 的结点
代码实现:
public class DoubleLinkedListDemo {
public static void main(String[] args) {
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.insert(1,new DNode(1));
doubleLinkedList.insert(1,new DNode(2));
doubleLinkedList.insert(1,new DNode(3));
doubleLinkedList.insert(3,new DNode(4));
// 插入位置不合法
doubleLinkedList.insert(5,new DNode(4));
doubleLinkedList.print();
System.out.println(doubleLinkedList.getLength());
}
}
// 双向链表
class DoubleLinkedList {
// 头结点
DNode dummyHead;
// 初始化链表
public DoubleLinkedList() {
this.dummyHead = new DNode(-1);
}
/**
* 获取链表长度
* */
public int getLength(){
int i = 0;
// 指向第一个首元结点
DNode p = dummyHead.next;
while (p != null) {
i++;
p = p.next;
}
return i ;
}
/**
* 双向链表的插入
* 在第 i 个位置插入 node 结点
* */
public void insert(int i,DNode node) {
int j = 0;
DNode p = dummyHead;
// 判断插入的是否是第一个结点
if (p.next == null && i == 1 ) {
p.next = node;
node.prior = p;
return;
}
// 判断插入位置是否合理
if (i <= 0 || i>getLength()) {
throw new IndexOutOfBoundsException("Index i is out of bounds");
}
while(p != null) {
// 无需找到 i-1 个位置,直接找到第i个位置
if (j == i) {
// 插入结点四步操作
node.prior = p.prior;
p.prior.next = node;
node.next = p;
p.prior = node ;
return;
}
p = p.next;
j++;
}
}
/**
* 打印结点
* */
public void print(){
DNode p = dummyHead.next;
while(p != null) {
System.out.print(p.data + " ");
p = p.next;
}
}
}
class DNode {
Object data;
DNode next; // 指向后继结点指针域
DNode prior; // 指向前驱结点的指针域
// 初始化一个结点
public DNode(Object data) {
this.data = data;
this.next = null;
this.prior = null;
}
@Override
public String toString() {
return "DNode{" +
"data=" + data +
", next=" + next +
", prior=" + prior +
'}';
}
}
(2)双向链表的删除
删除第 i 个位置的结点
代码实现:
public boolean delEle(int i) {
// 初始化:指向第一个结点
int j = 0;
DNode p = dummyHead;
while(p != null) {
// 找到删除结点的前一个结点
if (j == i ) {
// 删除
p.next.prior = p.prior;
p.prior.next = p.next;
return true;
}
// 后移
p = p.next;
j++;
}
return false;
}
2.6 顺序表和链表的比较
链式存储结构的优点
- 结点空间可以动态申请和释放
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素.
链式存储结构的缺点
存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
顺序表和链表的比较
2.7 线性表的应用
2.7.1 线性表的合并
算法步骤
- 遍历Lb,在 La 中查找是否存在Lb中的每一个元素
- 如果不存在,就将该元素插入到La的后面
单链表代码实现
// 构建链表La
LinkedList La = new LinkedList();
La.insertELe(1,7);
La.insertELe(2,5);
La.insertELe(3,3);
La.insertELe(4,11);
// 构建链表Lb
LinkedList Lb = new LinkedList();
Lb.insertELe(1,2);
Lb.insertELe(2,6);
Lb.insertELe(3,3);
// 将Lb合并到La
for (int i = 1; i <= Lb.length(); i++) {
// 遍历Lb,查看La中是否存在Lb中的元素
int index = La.locateEle(Lb.getELe(i));
if (index == -1) {
// 说明没有元素,将元素插入La的尾部
La.createList_tail(new Node(Lb.getELe(i)));
}
}
// 遍历La
La.print();
2.7.2 有序表的合并
(1)顺序表实现
算法步骤
- 创建一个空表Lc
- 依次从 La、Lb中摘取元素比较小的结点,插入到Lc的尾部,直到一个表为空
- 将不为空的表中的元素全部插入到 Lc 的尾部
// La
StructTypeImplementation La = new StructTypeImplementation(3);
La.insertElem(1,1);
La.insertElem(7,2);
La.insertElem(8,3);
// Lb
StructTypeImplementation Lb = new StructTypeImplementation(6);
Lb.insertElem(2,1);
Lb.insertElem(4,2);
Lb.insertElem(6,3);
Lb.insertElem(8,4);
Lb.insertElem(10,5);
Lb.insertElem(11,6);
// 创建一个空表Lc
StructTypeImplementation Lc = new StructTypeImplementation(La.getLength() +Lb.getLength());
// 使用俩个指针,分别指向La、Lb
int i = 1;
int j = 1;
// 只要有一个指向链表的尾端就停止
while (i <= La.getLength() && j <= Lb.getLength()) {
// 获取俩个链表中的元素
Integer a = (Integer) La.getElem(i);
Integer b = (Integer) Lb.getElem(j);
if (a < b) {
// 将a插入Lc尾部中
Lc.insertTail(a);
// 将La的指针向后移动
i++;
}else {
// 将b插入Lc中
Lc.insertTail(b);
// 将Lb的指针向后移动
j++;
}
}
if (i > La.getLength()) {
while(j <= Lb.getLength()) {
// 说明是La空了,直接将Lb尾部的结点插入到Lc
Lc.insertTail(Lb.getElem(j));
j++;
}
}else {
while(j <= Lb.getLength()) {
// 说明是Lb空了,直接将La尾部的结点插入到Lc
Lc.insertTail(La.getElem(j));
i++;
}
}
Lc.print();
插入尾部的方法:其余方法在线性表的顺序实现都说详细说明。
// 将元素插入到表的尾部
public void insertTail(Object data) {
for (int i = 0; i < this.elem.length; i++) {
if (elem[i] == null) {
System.out.println("插入的值" + data);
elem[i] = data;
return;
}
}
}
时间复杂度:O(La.length + Lb.length)
空间复杂度:O(La.length + Lb.length)
(2)链表实现
算法步骤
- 使用La或者Lb作为最终结果的链表,比如我用 La,下面为了区分,将最终的链表以 Lc 表示,其实就是La
- 使用三个指针,分别指向 La、Lb首元结点,Lc 的头结点
- 比较La、Lb中的每一个结点的 data 域,将较小的结点挂在 Lc 后边。将 pc 指向这个较小的结点,同时将 pc,pa或者pb后移
- 最后将不为空的链表直接挂载 Lc 的后面。
步骤分析
初始状态下:pc指向pa的头结点,pa、pb分别指向各自的首元结点
比较pa、pb指向结点的data域: pa.data < pb.data
将pa指向的结点挂到pc后边,同时更新pc、pa,具体操作为:
pc.next = pa;
pc= pa;
pa = pa.next;
此时继续循环上面的步骤…直到 La 链表为空,将Lb剩下的结点10、11挂在pc后边,具体的操作为:
// 如果pa不为空挂pa,否则挂pb
pc.next = pa != null ? pa :pb;
代码实现:
// 构建链表La
LinkedList La = new LinkedList();
La.insertELe(1,1);
La.insertELe(2,7);
La.insertELe(3,8);
// 构建链表Lb
LinkedList Lb = new LinkedList();
Lb.insertELe(1,2);
Lb.insertELe(2,4);
Lb.insertELe(3,6);
Lb.insertELe(4,8);
Lb.insertELe(5,10);
Lb.insertELe(6,11);
// 初始化指针
Node pa = La.dummyHead.next;
Node pb = Lb.dummyHead.next;
Node pc = La.dummyHead;
while(pa != null && pb != null) {
if (((Integer) pa.data) < ((Integer) pb.data)) {
// pa小,将pa挂载到pc后边
pc.next = pa;
// 将pc后移
pc = pa;
// 将pa指向下一个结点
pa = pa.next;
}else {
// pb小,将pb挂载到pc后边
pc.next = pb;
// 将pc后移
pc = pb;
// 将pb指向下一个结点
pb = pb.next;
}
}
// 最后将不为空的链表,挂载到pc后边
// 这个语句的意思: pa!=null将pa挂在到pc后边,否则挂pb
pc.next = pa != null ? pa :pb;
// 打印
La.print();