目录
简介
线性表是一种线性结构,它是具有相同类型的n(n≥0)个数据元素组成的有限序列,线性表的基本类型主要包括数组、链表、栈、队列等4种。本文先主要介绍其中的 数组和链表(单向链表、双向链表)的基础知识,并在最后给出双向链表的代码实现(Java语言实现)。
一、数组
数组是很常见的一种线性数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。数组中元素的逻辑顺序和存储顺序都是连续的,有上界和下界,数组的元素在上下界内是连续的。例如存储10,20,30,40,50的数组的示意图如下:
数组的特点是:逻辑结构和存储结构都是连续的,并且是顺序是对应的。也是因为这两个限制,使得数组的“随机访问”相对高效。同时这两个特点也带来的弊端是增删操作变得非常低效。原因是,为了保证连续性,需要做大量的数据搬移工作。
数组中稍微复杂一点的是多维数组和动态数组。多维数组本质上也是通过一维数组实现的。至于动态数组,是指数组的容量能动态增长的数组,一般都是,当当前数组的容量不够用的时候会自动创建一个新的数组(新数组的容量是当前数组的x倍,x一般是1.5,可以自己设置),然后将当前数组的数据复制到新数组中,这就实现了数组容量的自动增长。对于C语言而言,若要提供动态数组,需要手动实现;而对于C++而言,STL提供了Vector;对于Java而言,Collection集合中提供了ArrayList和Vector。
二、链表
链表与数组的区别是,它不像数组那样使用连续的内存空间,而是通过“指针”将一组零散的内存块串联起来。链表是由节点组成,每个节点除了包含当前元素的数据之外,还都包含指向下一个节点的指针,具体示意图在后面的介绍中都有。常见的链表有:单链表、双向链表和循环链表等。
2.1 单向链表
2.1.1 单向链表的概念
单向链表的示意图如下,单链表中的每个节点包含两部分:数据部分和后继节点指针部分。
单链表的特点是:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。
将单链表的中每一个节点看成一个整体,单链表的示意图可以表示如下:
表头的数据部分为空,表头的后继节点是"节点10"(数据为10的节点),"节点10"的后继节点是"节点20"(数据为10的节点),...,链表的最后一个节点的后继节点指针为空。
2.2.2 单链表删除节点
单向链表中删除节点的基本操作过程如下:
- 目的:删除"节点30"
- 删除之前:"节点20" 的后继节点为"节点30",而"节点30" 的后继节点为"节点40"。
- 删除之后:"节点20" 的后继节点为"节点40"。
2.1.3 单链表添加节点
单链表添加节点的基本操作过程如下:
- 目的:在"节点10"与"节点20"之间添加"节点15"
- 添加之前:"节点10" 的后继节点为"节点20"。
- 添加之后:"节点10" 的后继节点为"节点15",而"节点15" 的后继节点为"节点20"。
2.2 双向链表
2.2.1 双向链表的概念
和单链表一样,双链表也是由节点组成,只是双向链表中每个节点由3部分组成:前驱节点指针、数据、后继节点指针。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。它不像单链表那样只支持单项遍历,它可以支持双向遍历。
在双向链表中,前继节点指针为空的是表头,后继节点指针为空的是表尾。
双向链表的删除节点和插入节点跟单向链表基本一致,只是在删除和插入节点时加上对前驱节点指针的修改即可。
2.2.2 双链表删除节点
双向链表删除节点的基本操作过程:
- 目的:删除"节点30"
- 删除之前:"节点20"的后继节点为"节点30","节点30" 的前继节点为"节点20"。"节点30"的后继节点为"节点40","节点40" 的前继节点为"节点30"。
- 删除之后:"节点20"的后继节点为"节点40","节点40" 的前继节点为"节点20"。
2.2.3 双链表添加节点
双向链表添加节点的基本操作过程如下:
- 目的:在"节点10"与"节点20"之间添加"节点15"
- 添加之前:"节点10"的后继节点为"节点20","节点20" 的前继节点为"节点10"。
- 添加之后:"节点10"的后继节点为"节点15","节点15" 的前继节点为"节点10"。"节点15"的后继节点为"节点20","节点20" 的前继节点为"节点15"。
2.3 循环链表
循环链表是一种特殊的链表,它是将链表的结尾节点的后继指针指向头节点。下图所示是单链表的循环链表,同样,双链表也可以形成循环链表。
循环链表的添加与删除主要是要重点考虑结尾节点和 头结点的前继节点指针和后继节点指针问题,在这里就不详细描述了,大家可以自己画图展示一下。
2.4 循环双向链表的实现(Java实现)
/**
* Java 实现的双向链表,双向链表包含的基本操作如下:
* 构造双向链表
* 获取链表中节点个数
* 链表判空
* 获取表头
* 获取表尾
* 查询第i个节点数据
* 插入节点到指定位置
* 删除指定位置节点
* ...
*
* 注:java自带的集合包中有实现双向链表,路径是:java.util.LinkedList
*
* @author mukekeheart
* @date 2020/07/18
*/
public class DoubleLink<T> {
private DNode<T> mHead; // 表头
private int mCount; // 节点个数
// 构造函数
public DoubleLink() {
// 创建“表头”。注意:表头没有存储数据!
mHead = new DNode<T>(null, null, null);
mHead.prev = mHead.next = mHead;
// 初始化“节点个数”为0
mCount = 0;
}
// 返回节点数目
public int size() {
return mCount;
}
// 返回链表是否为空
public boolean isEmpty() {
return mCount==0;
}
// 获取index位置的节点
private DNode<T> getNode(int index) {
if (index<0 || index>=mCount)
throw new IndexOutOfBoundsException();
// 指定位置index在前半部分就 正向查找
if (index <= mCount/2) {
DNode<T> node = mHead.next;
for (int i=0; i<index; i++)
node = node.next;
return node;
}
// 指定位置index在后半部分就 反向查找
DNode<T> rnode = mHead.prev;
int rindex = mCount - index -1;
for (int j=0; j<rindex; j++)
rnode = rnode.prev;
return rnode;
}
// 获取第index位置的节点的值
public T get(int index) {
return getNode(index).value;
}
// 获取第1个节点的值
public T getFirst() {
return getNode(0).value;
}
// 获取最后一个节点的值
public T getLast() {
return getNode(mCount-1).value;
}
// 将节点插入到第index位置之前
public void insert(int index, T t) {
//如果插入的位置是最前面
if (index==0) {
DNode<T> node = new DNode<T>(t, mHead, mHead.next);
mHead.next.prev = node;
mHead.next = node;
mCount++;
return ;
}
DNode<T> inode = getNode(index);
DNode<T> tnode = new DNode<T>(t, inode.prev, inode);
inode.prev.next = tnode;
inode.prev = tnode;
mCount++;
return ;
}
// 将节点插入第一个节点处。
public void insertFirst(T t) {
insert(0, t);
}
// 将节点追加到链表的末尾
public void appendLast(T t) {
DNode<T> node = new DNode<T>(t, mHead.prev, mHead);
mHead.prev.next = node;
mHead.prev = node;
mCount++;
}
// 删除index位置的节点
public void delNode(int index) {
DNode<T> inode = getNode(index);
inode.prev.next = inode.next;
inode.next.prev = inode.prev;
inode = null;
mCount--;
}
}
class DNode<T> {
public DNode<T> prev; //前驱节点指针
public DNode<T> next; //后继节点指针
public T value; //节点数据
//双向节点构造函数
public DNode(T value, DNode prev, DNode next) {
this.value = value;
this.prev = prev;
this.next = next;
}
}
三、总结:数组与链表的特点
数组和链表由于其存储特点不一样,所以两个在不同方面表现的优缺点也有所不同。下面是一个简单的总结:
- 数组优点: 随机访问性强,查找速度快(连续内存空间导致的);
- 数组缺点: 插入和删除效率低 可能浪费内存 内存空间要求高,必须有足够的连续内存空间。数组大小固定,不能动态拓展
- 链表的优点: 插入删除速度快,内存利用率高,不会浪费内存 大小没有固定,拓展很灵活。(每一个数据存储了下一个数据的地址,增删效率高)
- 链表的缺点:不能随机查找,必须从第一个开始遍历,查找效率低