1. 顺序存储结构不足的解决办法
上一篇博客中说的是线性表的 顺序存储结构。它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。能不能想办法解决呢?
要解决这个问题,我们就得考虑一下 致这个问题的原因。
相邻两元素的存储位置也具有邻居关系。他们的编号是0, 1, 2, 3…, n。他们在内存中的位置也是紧挨着的,中间没有空隙,当然也就无法快速介入,而删除后,当中就会留出空隙,自然也就需要弥补。
单链表思路
我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址) ,而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
2. 线性表链式存储结构定义
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在 存未被占用的任意位置
以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构 ,除了要存数据元素信息外还要存储它的后继元素的存储地址。
因此, 为了表示每个数据 ai 接后继数据 ai+1 间的逻辑数据关系,对数据元素 ai 来说 除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素 ai 的存储映像,称为节点(Node)。
n 个节点( ai 的存储映像)链接成一个链表,即为线性表(a1, a2, a3…, an)的链式存储结构,因此链表的每个节点中只包含一个指针域,所以叫做单链表。单链表正是通过每个节点的指针域将线性表的数据元素按期逻辑次序连接在一起。
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个节点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。想象一下,最后一个结点,它的指针指向哪里?
最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为 “空” (通常用 NULL
或 “^
” 符号表示)
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息 ,谁叫它是第一个呢,有这个特权。也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针。
3. 头指针与头结点的异同
头指针 | 头结点 |
---|---|
头指针是指链表指向第一个节点的指针。若链表有头节点,则只想头结点的指针 | 头节点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据一般无意义(也可以存放链表的长度) |
头指针具有标识作用,所以常用头指针冠以链表的名字 | 有了头节点,对在第一个元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了 |
无论链表是否为空,头指针均不为空。头指针是链表的必要元素 | 头节点一定是链表的必要元素 |
单链表为空
这里我们大概地用图示表达了内存中单链表的存储状态。看着满图的省略号"… …n”,就知道是多么不方便。而我们真正关心的:它是在内存中的实际位置吗?不是的,这只是它所表示的线性表中的数据元素及数据元素之间的逻辑关系。所以我们改用更方便的存储示意图来表示
单链表
带有头结点的单链表
空链表
单链表中,在 Java 语言中可用类来描述
class Node01 {
public int data;
public Node01 next;
public Node01(int data) {
this.data = data;
}
public Node01 head;
}
从这个类的定义中,我们也就知道,结点由存放数据元素的数据域和存放后继结点地址的指针域组成。假设 p 是指向线性表第 i 个元素的指针,则该结点 ai 的数据域可以用 p.data 来表示,p.data 的值是一个 int 型的数据元素,结点 ai 的指针域可以用 p.next 来表示
4. 常用单链表操作方法
4.1 头插法
// 头插法
public void addFirst(int data) {
Node01 node = new Node01(data);
//判断是否为第一次插入
if (this.head == null) {
this.head = node;
} else {
node.next = this.head;
this.head = node;
}
}
算法思路:
- 创建一个插入的结点 node
- 判断是否为第一次插入
- 如果是第一次插入,那么头结点 head 直接指向新创建的 node 结点即可
- 如果非第一次插入,那么新创建的 node 必须先和后继结点绑定然后头结点 head 再指向 node,node变为头结点
4.2 尾插法
//尾插法
public void addLast(int data) {
Node01 node = new Node01(data);
//判断是否为第一次插入
if (this.head == null) {
this.head = node;
} else {
Node01 cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
算法思路:
- 先创建一个新结点 node
- 判断是否为第一次插入
- 如果是第一次插入,则头结点 head 指向新结点 node, node变为 head 结点
- 如果非第一次插入,先创建一个游动的 cur 结点,初始值为头节点 head,while循环到最后一个结点后由于 next 域为空,所以 cur 就是最后的尾巴结点
- 让尾巴结点的 next 域指向 新创建的 node 结点即可
4.3 任意位置插入
//任意位置插入
public void addIndex(int index, int data) {
if (index == 0) {
addFirst(data);
} else if (index == size()) {
addLast(data);
} else if (index > 0 && index < size()) {
Node01 node = new Node01(data);
Node01 cur = this.head;
while ((index - 2) != 0) {
cur = cur.next;
--index;
}
node.next = cur.next;
cur.next = node;
}
}
算法思路:
- 先解决头插和尾插后再解决中间插入
- index-1 是为了消除0下标开始的误差1,index-1-1是为了在正确的位置插入:加入需要插入第二个元素位置,我们不需要 移动链表之间的关系,直接把 node 的 next 域存储 head 的 next域,然后 head 的 next 域指向 node 即可实现第二个元素的插入。不需要进行类似于“移动”的操作,以此类推,所以需要额外进行“减一”
- while找到要插入元素之前的元素cur,node 先绑定 cur.next 的结点,然后 cur.next 再指向 node
4.4 遍历一次删除第一次出现关键字为 key 的节点
//遍历一次删除第一次出现关键字为 key 的节点
public void remove(int key){
//判断是否为空链表
if (this.head == null){
return;
}
//如果值和头结点相等则先删除头节点
if (this.head.data == key){
this.head = this.head.next;
return;
}
//在删除其它节点:找到被删除结点的前驱结点
Node01 prev = this.head;
while (prev.next != null){
if (prev.next.data == key){
//需要注意是prev.next.data而不是prev.data
break;
}
prev = prev.next;
}
Node01 del = prev.next;
prev.next = del.next;//前驱绑定后驱
}
算法思路:
- 先判断是否为空链表
- 因为只是删除第一次出现的元素,所以可以先删除和头结点值相等的结点,并让第二个元素结点当头结点
- 如果头结点不是要删除的结点再删除其它节点:先找被删除结点的前驱 prev;找到后再将其前驱结点绑定后驱结点
while (prev.next != null)用prev.next而不是prev是为了找到被删除结点的前驱结点,找到后break退出循环。prev.next就是被删除的结点,赋值给del。被删除结点的后驱结点域被删除结点的前驱结点相连接,prev.next = del.next
4.5 遍历一次,删除所有关键字为 key 的节点
//遍历一次,删除所有关键字为 key 的节点
public void removeAll(int key){
Node01 prev = this.head;
Node01 cur = this.head.next;
//先删除头节点以外的节点
while (cur != null){
if (cur.data == key){
prev.next = cur.next;
}else{
prev = cur;
}
cur = cur.next;
}
//在删除值与头结点相等
if (this.head.data == key){
this.head = this.head.next;
}
}
算法思路:
- 先删除头结点以外的结点
- 再判断是否删除头结点
.如果第一步就删除头节点,[head = head.next]会造成prev的节点无法删除: 当this.head == this.head.next时,之前定义的Node prev = this.head;就会残留下来不会被JVM垃圾回收机制回收掉
while (cur != null)依旧使用cur而不是cur.next是因为:cur在定义的时候是从head.next开始,如果继续cur.next的话就会跳过第二个元素
if (cur.data == key): 找到结点data值相等就开始将此节点的后驱给前结点的后驱[假设有结点1, 2, 3我们删除2,就是把2的后驱3赋值给2的前驱1,2结点被链表抛出]。然后cur.next继续向下寻找而prev原地不动,保留被删除结点的前驱,如果相等就继续删除,如果不相等,prev前驱会被cur赋值,然后cur.next继续向后走
4.6 查找关键字 key
//查找关键字 key
public boolean contains(int key) {
Node01 cur = this.head;
while (cur != null) {
if (cur.data == key) {
return true;
}
cur = cur.next;
}
return false;
}
4.7 链表长度
//链表长度
public int size() {
Node01 cur = this.head;
int len = 0;
while (cur != null) {
cur = cur.next;
++len;
}
return len;
}
4.8 输出单链表
//输出单链表
public void display() {
Node01 cur = this.head;
while (cur != null) {
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
4.9 清空单链表
//青空单链表
public void clear() {
this.head = null;
}
Java的JVM垃圾回收:如果结点没有前驱就会被自动回收,因此当头结点为空,则后虚的结点发生多米诺效应,全部被JVM回收
5. 完整代码
package dataStructure.list;
/**
* Name: Demo07单链表
* User: cxf
* Date: 2021/8/6
* Time: 9:03 上午
* Description:
*/
class Node01 {
public int data;
public Node01 next;
public Node01(int data) {
this.data = data;
}
public Node01 head;
// 头插法
public void addFirst(int data) {
Node01 node = new Node01(data);
//判断是否为第一次插入
if (this.head == null) {
this.head = node;
} else {
node.next = this.head;
this.head = node;
}
}
//尾插法
public void addLast(int data) {
Node01 node = new Node01(data);
//判断是否为第一次插入
if (this.head == null) {
this.head = node;
} else {
Node01 cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
//任意位置插入
public void addIndex(int index, int data) {
if (index == 0) {
addFirst(data);
} else if (index == size()) {
addLast(data);
} else if (index > 0 && index < size()) {
Node01 node = new Node01(data);
Node01 cur = this.head;
while ((index - 2) != 0) {
cur = cur.next;
--index;
}
node.next = cur.next;
cur.next = node;
}
}
//查找关键字 key
public boolean contains(int key) {
Node01 cur = this.head;
while (cur != null) {
if (cur.data == key) {
return true;
}
cur = cur.next;
}
return false;
}
//链表长度
public int size() {
Node01 cur = this.head;
int len = 0;
while (cur != null) {
cur = cur.next;
++len;
}
return len;
}
//遍历一次删除第一次出现关键字为 key 的节点
public void remove(int key) {
//判断是否为空链表
if (this.head == null) {
return;
}
//先删除头节点
if (this.head.data == key) {
this.head = this.head.next;
return;
}
//在删除其它节点
Node01 prev = this.head;
while (prev.next != null) {
if (prev.next.data == key) {
break;
}
prev = prev.next;
}
Node01 del = prev.next;
prev.next = del.next;
}
//遍历一次,删除所有关键字为 key 的节点
public void removeAll(int key) {
Node01 prev = this.head;
Node01 cur = this.head.next;
//先删除头节点以外的节点
while (cur != null) {
if (cur.data == key) {
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
//在删除值与头结点相等
if (this.head.data == key) {
this.head = this.head.next;
}
}
//输出单链表
public void display() {
Node01 cur = this.head;
while (cur != null) {
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
//清空单链表
public void clear() {
this.head = null;
}
}
public class linkedList01 {
public static void testAddFirst() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addFirst(i);
}
node.display();
}
public static void testAddLast() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
node.display();
}
public static void testAddIndex() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addIndex(i, i);
}
node.display();
}
public static void testRemove() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addLast(0);
}
node.remove(0);
node.display();
}
public static void testRemoveAll() {
Node01 node = new Node01(-1);
for (int i = 0; i < 5; i++) {
node.addLast(0);
}
for (int i = 0; i < 5; i++) {
node.addLast(1);
}
node.removeAll(0);
node.display();
}
public static void testContains() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addLast(i);
System.out.println(i + ": " + node.contains(i));
}
}
public static void testSize() {
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
System.out.println("node.size: " + node.size());
}
public static void testClear(){
Node01 node = new Node01(-1);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
System.out.print("清空前:");
node.display();
node.clear();
System.out.print("清空后: ");
node.display();
}
public static void main(String[] args) {
// 单元测试用例
}
}
6. 泛型单链表
较之于讲解的单链表在于能够适应任何数据类型并且封装的更好。
其它简单的泛型单链表操作省略,可参考讲解中的操作
package dataStructure.list;
/**
* Name: linkedList
* User: cxf
* Date: 2021/8/30
* Time: 8:12 下午
* Description:
*/
class linkedListOutOfIndexRange extends RuntimeException {
linkedListOutOfIndexRange(String message) {
super(message);
}
}
class Node02<T> {
private T data;
private Node02 next;
Node02(T data) {
this.data = data;
}
private Node02 head;
// 头插
void addFirst(T data) {
Node02 node = new Node02(data);
if (this.head == null) {
this.head = node;
} else {
node.next = this.head;
this.head = node;
}
}
// 尾插
void addLast(T data) {
Node02 node = new Node02(data);
if (this.head == null) {
this.head = node;
} else {
Node02 cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
}
// 任意位置插入
void addIndex(int index, T data) {
if (index == 0) {
addFirst(data);
} else if (index == size()) {
addLast(data);
} else if (index > 0 && index < size()) {
Node02 cur = this.head;
Node02 node = new Node02(data);
while ((index - 1) != 0) {
cur = cur.next;
--index;
}
node.next = cur.next;
cur.next = node;
} else {
throw new linkedListOutOfIndexRange(index + "越界");
}
}
// 删除第一次出现的元素值
void remove(T key) {
if (this.head == null) {
return;
} else if (this.head.data == key || this.head.data.equals(key)) {
this.head = this.head.next;
} else {
Node02 prev = this.head;
while (prev.next != null) {
if (prev.next.data == key || prev.next.data.equals(key)) {
break;
}
prev = prev.next;
}
if (prev.next != null) {
Node02 del = prev.next;
prev.next = del.next;
}
}
}
// 删除所有元素值
void removeAll(T key) {
/*
删除全部 key 后删除 head 的原因:
*/
if (this.head == null) {
return;
} else {
Node02 prev = this.head;
Node02 cur = this.head.next;
while (cur != null) {
if (cur.data == key || cur.data.equals(key)) {
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
if (this.head.data == key || this.head.data.equals(key)) {
this.head = this.head.next;
}
}
}
// 打印
void display() {
Node02 cur = this.head;
while (cur != null) {
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
// 元素是否存在
boolean contains(T data) {
Node02 cur = this.head;
while (cur != null) {
if (cur.data == data || cur.data.equals(data)) {
return true;
}
cur = cur.next;
}
return false;
}
// 获取单链表长度
int size() {
int count = 0;
Node02 cur = this.head;
while (cur != null) {
cur = cur.next;
++count;
}
return count;
}
// 清空单链表
void clear() {
this.head = null;
}
}
public class linkedList02 {
private static void testAddFirst() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addFirst(i);
}
node.display();
}
private static void testAddLast() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
node.display();
}
private static void testAddIndex() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
node.addIndex(4, 4);
node.display();
}
private static void testRemove() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
for (int i = 5; i < 15; i++) {
node.remove(i);
}
node.display();
}
private static void testRemoveAll() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
node.addLast(0);
node.addLast(0);
node.addLast(0);
node.addLast(0);
node.addLast(0);
node.removeAll(0);
node.display();
}
private static void testContains() {
Node02 node = new Node02(null);
for (int i = 0; i < 10; i++) {
node.addLast(i);
}
for (int i = 0; i < 15; i++) {
System.out.println("contains(" + i + "): " + node.contains(i));
}
}
public static void main(String[] args) {
// 放置需要测试的用例
}
}
7. 单链表结构与顺序存储结构优缺点
存储分配方式 | 时间性能 | 空间性能 |
---|---|---|
顺序存储结构用一段连续的存储单元依次存储线性表的数据元素 | 查找:顺序存储结构O(1);单链表O(N) | 顺序存储结构需要分配存储空间,分大了,浪费,分小了易发生上溢 |
单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素 | 插入和删除:顺序存储结构需要平均移动表长一半的元素,时间为O(N);单链表在算出某位置的指针后,插入和删除时间仅为O(1) | 单链表不需分配存储空间,只要有就可以分配,元素个数也不受限制【机器内存决定】 |
通过上面的对比,我们可以得出一些经验性的结论:
- 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结。若需要频繁插入和删除时,宜采用单链表结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构。而游戏中的玩家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不大合适了,单链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构,这样可以不需要考虑存储空间的大小问题。而如果事先知道线性表的大致长度,比如一年12个月,一周就是星期一至星期日共七天,这种用顺序存储结构效率会高很多。
总之,线性表的顺序存储结构和单链表结构各有其优缺点 不能简单的说哪个好,哪个不好,需要根据实际情况,来综合平衡采用哪种数据结构更能满足和达到需求和性能。