线性表 - 链表(单链表)
1.1 链表的介绍
-
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成
-
链表中数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
-
单链表
- 单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next
存储原理
- 单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next
-
数组在内存中的存储方式是顺序存储(连续存储),链表在内存中的存储方式则是随机存储(链式存储)
-
链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间
-
链表的第1个节点被称为头节点(3),没有任何节点的next指针指向它,或者说它的前置节点为空头结点用来记录链表的基地址。有了它,我们就可以遍历得到整条链表链表的最后1个节点被称为尾节点(2),它指向的next为空
1.2 链表常用操作
操作
-
查找节点
- 在查找元素时,链表只能从头节点开始向后一个一个节点逐一查找
- 在查找元素时,链表只能从头节点开始向后一个一个节点逐一查找
-
更新节点
- 找到要更新的节点,然后把旧数据替换成新数据
- 找到要更新的节点,然后把旧数据替换成新数据
-
插入节点
-
尾部插入
- 把最后一个节点的next指针指向新插入的节点即可
- 把最后一个节点的next指针指向新插入的节点即可
-
头部插入
- 第1步,把新节点的next指针指向原先的头节点
- 第2步,把新节点变为链表的头节点
-
中间插入
-
第1步,新节点的next指针,指向插入位置的节点
-
第2步,插入位置前置节点的next指针,指向新节点
-
只要内存空间允许,能够插入链表的元素是无限的,不需要像数组那样考虑扩容的问题
-
-
-
删除节点
-
尾部删除
- 把倒数第2个节点的next指针指向空即可
- 把倒数第2个节点的next指针指向空即可
-
头部删除
- 把链表的头节点设为原先头节点的next指针即可
- 把链表的头节点设为原先头节点的next指针即可
-
中间删除
- 把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可
- 把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可
-
1.3 编写代码实现链表常用操作
package com.lagou.entity;
/**
* @author 云梦归遥
* @date 2022/5/12 11:34
* @description 单链表
*/
// 对单链表进行相关实现
public class SingleLink {
// 单链表
public class MyLink{
private int value; // 数据域
private MyLink link; // 指针域
public MyLink(int num){
this.value = num;
this.link = null;
}
public MyLink(int num, MyLink node){
this.value = num;
this.link = node;
}
}
// 记录链表的长度(因为头结点的数据域保存着链表的信息)
private MyLink head = new MyLink(0, null); // 头结点
// 增加节点
// 尾插增加节点(遍历获取最后一个节点)
public void addNodeByOrder(MyLink node){
MyLink temp = head; // 创建临时节点,进行链表的遍历
// 更新链表长度
if (head.value == 0){
// 空链表
head.link = node; // 更新指针域
} else {
// 链表不为空,则需要找到链表的尾节点,进行更新
while (true){
if (temp.link == null){
break;
} else {
temp = temp.link; // 继续遍历下一个节点
}
}
// 此时获取到了尾节点
temp.link = node; // 更新指针域
}
head.value++; // 更新链表长度
}
// 头插增加节点(插入到头节点的后面,首元节点的前面)
public void addNodeByHead(MyLink node){
if (head.value == 0){
// 空链表
head.link = node; // 更新指针域
} else {
// 进行指针域,链表长度的更新
node.link = head.link;
head.link = node;
}
head.value++;
}
// 中间插入
public void addNodeByMiddle(int index, MyLink node){
MyLink temp = head; // 创建临时节点,进行链表的遍历
if (head.value <= index){
while (true){
if (temp.link == null){
break;
} else {
temp = temp.link; // 继续遍历下一个节点
}
}
temp.link = node;
} else {
// 遍历到指定的节点
for (int i = 1; i <= index; i++){
temp = temp.link;
}
node.link = temp.link;
temp.link = node;
}
head.value++;
}
// 删除节点
// 尾删
public MyLink deleteByTail(){
MyLink temp = head; // 创建临时节点,进行链表的遍历
if (head.value == 0){
return null;
} else {
// 循环遍历尾节点
while (true){
if (temp.link != null && temp.link.link == null){
break;
} else {
temp = temp.link;
}
}
MyLink deleteNode = temp.link;
temp.link = null;
head.value--;
return deleteNode;
}
}
// 头删
public MyLink deleteByHead(){
MyLink deleteNode = null;
if (head.value == 0){
return null;
} else if (head.value == 1){
deleteNode = head.link;
head.link = null;
head.value--;
return deleteNode;
} else {
deleteNode = head.link;
head.link = head.link.link;
head.value--;
return deleteNode;
}
}
// 中间删除
public MyLink deleteByMiddle(int index){
MyLink temp = head;
MyLink deleteNode = null;
if (head.value < index){
return null;
} else {
for (int i = 1; i < index; i++){
temp = temp.link;
}
if (head.value == index){
deleteNode = temp.link;
temp.link = null;
return deleteNode;
} else {
deleteNode = temp.link;
temp.link = temp.link.link;
return deleteNode;
}
}
}
// 更新
public MyLink update(int index, int value){
MyLink temp = head;
MyLink updateNode = null;
if (head.value <= index){
return null;
} else{
for (int i = 1; i <= index; i++){
temp = temp.link;
}
updateNode = temp;
temp.value = value;
return updateNode;
}
}
// 查询
public MyLink select(int index){
MyLink temp = head;
MyLink selectNode = null;
if (head.value <= index){
return null;
} else{
for (int i = 1; i <= index; i++){
temp = temp.link;
}
selectNode = temp;
return selectNode;
}
}
// 查询整个链表
public String select(){
MyLink temp = head;
StringBuilder stringBuilder = new StringBuilder();
if (head.value == 0){
return null;
} else{
stringBuilder.append("【链表长度:" + head.value + "】");
temp = temp.link;
while (true){
if (temp.link == null){
stringBuilder.append(temp.value);
break;
} else {
stringBuilder.append(temp.value + " => ");
temp = temp.link;
}
}
return stringBuilder.toString();
}
}
}
进行测试上述功能
package com.lagou.test;
import com.lagou.entity.SingleLink;
/**
* @author 云梦归遥
* @date 2022/5/12 13:25
* @description
*/
public class SingleLinkTest {
public static void main(String[] args) {
SingleLink singleLink = new SingleLink();
SingleLink.MyLink myLink1 = singleLink.new MyLink(1);
SingleLink.MyLink myLink2 = singleLink.new MyLink(2);
SingleLink.MyLink myLink3 = singleLink.new MyLink(3);
// 尾插 3 个节点
singleLink.addNodeByOrder(myLink1);
singleLink.addNodeByOrder(myLink2);
singleLink.addNodeByOrder(myLink3);
System.out.println(singleLink.select());
SingleLink.MyLink myLink4 = singleLink.new MyLink(4);
// 头插 节点
singleLink.addNodeByHead(myLink4);
SingleLink.MyLink myLink5 = singleLink.new MyLink(5);
// 中间插 1 个节点
singleLink.addNodeByMiddle(2, myLink5);
System.out.println(singleLink.select());
// 更新节点
singleLink.update(4, 6);
System.out.println(singleLink.select());
singleLink.deleteByTail(); // 尾删 节点
singleLink.deleteByHead(); // 头删 节点
singleLink.deleteByMiddle(2); // 中间删 节点
System.out.println(singleLink.select());
}
}
测试结果
1.4 链表总结
-
时间复杂度
- 查找节点 : O(n)
- 插入节点:头插O(1),尾插O(1),中间插O(n)
- 更新节点:O(n)
- 删除节点:头删O(1),尾删O(1),中间删O(n)
-
优缺点
- 优势
- 插入、删除、更新效率高,省空间
- 劣势
- 查询效率较低,不能随机访问
- 优势
-
应用
- 链表的应用也非常广泛,比如树、图、Redis的列表、LRU算法实现、消息队列等
-
数组与链表的对比
- 数据结构没有绝对的好与坏,数组和链表各有千秋
- 数组的优势在于能够快速定位元素,对于读操作多、写操作少的场景来说,用数组更合适一些
- 链表的优势在于能够灵活地进行插入和删除操作,如果需要在尾部频繁插入、删除元素,用链表更合适一些