线性表链式存储结构
- 线性表的链式存储结构,与顺序结构中的数组不同的是,不需要开辟连续的内存,链式,主要是内存中一些比较离散的数据通过存储相互之间的地址,来联系到一起。每个数据元素除了存储自己的数据以外,还需要存储后继元素的地址。**注意:这里说的是后继。因为有可能是双向链表。**下图就是一个链表。前面也说过,链式比顺序好的一点是,删除元素,添加元素相对于顺序比较容易。
单链表
- 单向链表,只有一个方向的链表,每个节点存放的地址只有一个它的后继元素的地址。比如:下图就是一个单项链表。每个节点都存有他本身的元素(data),以及的的后继元素的地址(next).比较容易理解。
- 所以现在已经明白什么是链式存储结构,什么是单链表了,然后之后要考虑的是,要用代码怎么实现这个单链表。
- 既然链表每一个节点都是独立的,可以建立一个节点,让这个节点包含data域和指针域,所以可以想到定义一个类,首先明白一点,我们实现的是链表,因此用户不需要明白底层源码是如何实现的,所以这个节点类(Node)是私有的,而且我们有必须使用这个节点类,所以我们可以想到内部类。
private class Node {//内部类Node
E data;// 数据域
Node next;// 指针域
public Node() {//构造方法初始话节点为空
this(null, null);
}
public Node(E data, Node next) {
this.data = data;
this.next = next;
}
@Override//重写的toString()方法
public String toString() {
return data.toString();
}
}
- 节点也建立好了,之后要考虑的是如何将这些数据连接到一起,按照最简单的思维,比如有一个新元素,我们要加到链表中,之后陆续来了一些新元素,我们要接入,但是我们应该想到链表从何时开始,又从何是结束,这是我们应该设置两个标记,也就是头指针,尾指针来标记一下,头尾指针就是用来记录头节点和尾结点的。存储头尾结点的地址。
这里有一点需要注意,尾指针没有意外指的是链表尾元素。但是头指针却不同。有两种情况需要注意一下。就是虚拟头节点和真实头结点。
虚拟头节点:也被称为头节点,但是越没有元素,只是标记一下头指针的位置,当中的数据域没有存储任何元素值得注意在学校时,也有数据结构的课程,不过是C语言数据结构,中间也有链表中的,头结点的课程,在其中,建立一个链表是,用的最多的就是虚拟头节点,从下一个节点开始存元素,这才是实际意义上的节点。与C语言数据结构上的一样,在单链表中我们还是用虚拟的多一点。
真实头节点:有实际意义的头节点,与每个节点都一样,有数据,也都下一节点的地址。
4. 到此,头尾结点,指针都已经设立好了,之后就是给链表里面加元素了。在链表中,有两种最基本的添加元素的方法头插法和尾插法。下面是单链表中最重要的几个方法!
头插法:每次都在首节点之前添加元素,每次添加元素之后新元素都为首节点。注意:这里我说的是首元素,是为了区分头节点,因为使用的是虚拟头节点,所以我这里所说的首节点是头节点的下一个节点。
//头插法核心代码
Node n = new Node(e, null);//将要插入的新元素
if (index == 0) { // 头插
n.next = head.next;//先将头结点的下一个元素的地址给新结点
head.next = n;//将n的地址给头结点的next
if (size == 0) {
rear = n;
}
尾插法:
if (index == size) { // 尾插
rear.next = n;//将n的地址给尾指针的next
rear = n;//尾结点后移,指向新的节点n
}
一般插入
Node p = head;//申请一个游标指针,以便于找待插入的位置
//从头节点依次往后移直到待插入位置的前一个节点
for (int i = 0; i < index; i++) {
p = p.next;//指针往后移
}
n.next = p.next;将待插位置的后一个节点的地址给n,
p.next = n;//最后将新结点的地址给p的下一跳,就完成了一般插入操作
当然我们也可以将头插尾插,以及一般插入结合起来。
5. 然后还有get(int index)功能,就是获取指定位置的元素。获取指定角标数据时,首先要做的就是判断指定角标是否越界,之后,因为实现的是链表,所以没有角标,因此只能通过遍历来实现查找元素的功能。目前还没有办法优化。
public E get(int index) {
//判断指定位置角标是否越界。
if (index < 0 || index >= size) {
//如果越界,抛出角标越界异常
throw new IllegalArgumentException("插入角标非法");
}
if (index == 0) {
//获取首元素
return head.next.data;
} else {
Node p = head;
for (int i = 0; i <= index; i++) {
p = p.next;
}
return p.data;
}
}
- 之后重要的几个功能还有删除功能。
这时候要注意的是,要对删除位置进项判断
(1). 如果角标越界是不可以进行删除的。
if (index < 0 || index >= size) {
throw new IllegalArgumentException("");
}
(2). 删除的链表中只有一个节点。
(3). 删除的是首节点。
if (index == 0) {// 头删
Node p = head.next;//先申请一个节点用来指向将要删除的头节点。
res = p.data;//用于返回的值
head.next = p.next;//将删除的节点的后继结点的地址给头节点
p.next = null;//因为此时p还指向它的后继节点,所以置空。
p = null;
if (size == 1) {//如果此时只有一个节点,尾指针要指向头节点。
rear = head;
}
size--;//删除一个节点后,总长度要减一。
return res;
}
(4). 删除的是尾结点。
删除尾结点是比较简单的。只需要将尾指针的前一个节点的指针域置空,尾指针往前移就好了。当然,要找尾结点的前一个节点,必须循环遍历。
if (index == size - 1) {//尾删
res = rear.data;//用来接要删除的节点的值。
Node p = head;//游标节点,用来找尾结点的前一个结点。
while (p.next != rear) {
p = p.next;
}
p.next = null;//倒数第二个节点置空
rear = p;//尾指针前移
size--;//重要:长度减一
return res;
}
(5). 删除的节点位置在链表中间(一般删除)。
删除这种不特殊位置的节点时,应该先找这个待删除的节点,之后在进行删除,找节点是还是需要使用循环,此时用for循环比较合适。
//一般删除
Node p = head;
for (int i = 0; i < index; i++) {//用for循环寻找待删除元素的前驱节点
p = p.next;
}
Node del = p.next;
res = del.data;
p.next = p.next.next;//待删节点的后继节点的地址赋值给带删节点的前驱节点
size--;//长度减一
del.next = null;//这也可以有,也可以没有,将待删节点的指针域置空,此时已完成删除
return res;
这就是建立单链表的大致过程。