四-上, 单链表(Linked List)
4.1 定义和栗子
- 定义
维基百科:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存储下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
- 链表的特点:
动态分配内存空间, 分散存储数据, 依靠链式顺序访问, 因为元素之间存在链式链接, 所以插入删除快, 但是查找效率低
- 应用场景:
链表适合于 存储 对元素查找,访问要求低;但是对删除,插入要求高的数据。
- 链表的种类: 单,双,循环单,循环双链表等
4.2 单链表
4.2.1 单链表的定义,结构和特点
- 结点中只有一个指针域的链表叫单链表;
- 单链表结点的基本组成: 数据域用来存储数据,指针域用来存储下一个结点的位置信息;
头结点,头指针和首元节点
头结点:
- 头结点:有时,在链表的首元结点之前会额外增设一个结点,结点的数据域一般不存放数据(有些情况下也可以存放链表的长度等信息),此结点被称为头结点。
- 头结点指针域为空的时候,说明链表为空表;
- 头结点在单链表中可有可无,但是主要是为了方便运算的实现:
- 头结点可以使得
链表首元结点的插入,删除和其他节点一样(因为都有前驱结点)
- 头结点可以
使非空链表和空链表的操作相同(因为链表中都有了元素,空链表中包含了头结点)
- 头结点可以使得
头指针:
- 永远指向链表中第一个结点的位置(如果链表有头结点,头指针指向头结点;否则,头指针指向首元结点)。
首元结点:
- 链表中第一个元素所在的结点,它是头结点后边的第一个结点。
4.2.2 单链表常用方法的具体实现
在JAVA实现写单链表时,我们通常会在链表类中持有一个 结点内部类供链表类使用,
这个内部类包含了: 存储下一个结点位置的next指针域, 若干数据域,结点类的构造方法(初始化数据域) 以及重写的toString()方法
结点内部类的定义
class LinkedList{
class Node{
Node next;
Data data;
public class Node(Data data){
this.data= data;
}
public string toString(){
return "";
}
}
}
4.2.2.1 单链表的链接和打印的具体实现
单链表插入有头插法和尾插法两种. 当我们使用尾插法时, 从链表末端加入结点;
单链表加入新结点(链接)的实现思想:
- 先创建一个头结点head,
头结点的作用是标识单链表的开头,注意后面不能有任何移动的行为!
;- 我们添加新的结点时,从头结点head遍历至链表的末尾;
那么如何遍历链表呢?
- 新建一个结点 temp,初始时候指向head结点(Node temp = head;) 让他通过调用next域,实现对链表中结点的遍历;
点这里查看next域的详细解释
代码示例:
///单链表的链接(尾插法)(带头结点)
Node head = new Node(0,"");//初始化头结点,事实上这个是作为链表类的成员变量来使用的
public void add(Node _node){
Node temp=head;///临时Node结点,用来遍历访问Node
while(true){
if(temp.next == null)
break;
temp = temp.next;
}
temp.next = _node;
}
///链表的输出
public void show(){
//临时结点
Node temp = head;
//
while(true){
if(temp.next == null){
System.out.println("链表为空!");
break;
}
System.out.println(temp.next);
temp = temp.next;
}
}
4.2.2.2 单链表的修改和删除的具体实现
- 学过数据结构我们都知道单链表的插入,删除和修改的伪代码写法:
- 这里为了更贴合Java的实际实现,我们只用并且一直用(每个方法内都新建一个temp来使用)temp临时变量遍历结点
///1.定点插入-如果我们插入一个新结点,
寻找插入位置,找到后修改引用关系即可
node.next = temp.next;//node为新插入的结点
temp.next = node;
///2.删除-如果我们要删除某个结点,只需要找到这个结点的前一个结点,然后改变其指针域到这个被删除结点的下一个节点即可
temp.next = temp.next.next;
//3.修改-如果我们要修改某个结点的数据域,那就遍历搜索到这个节点,然后用结点.属性修改即可
temp.data=xxx;
具体实现代码如下:
1.结点的定点插入
/*
node.next = temp.next;
temp.next = node;
*/
public void insertNode(Node node,String name){
///查找插入位置
/ 比如我们插入到name="小狗"的后面
///临时变量
Node temp= head;
boolean flag = false;
if (temp.next == null){
System.out.println("链表为空,无法找到插入位置");
}
while(true){
if(temp.next.name.equals(name)){
flag=true;
break;
}
temp = temp.next;
}
if(flag){
temp = temp.next; //找到目标name
node.next = temp.next;
temp.next = node;
}
}
2.结点的删除
/*
temp.next =temp.next.next;
*/
public void deleteNode(Node node){
///临时结点
Node temp = head;
//查找结点位置
while(true){
if (temp.next == null){
System.out.println("查找失败");
break;
}
if(temp.next == node){
temp.next = temp.next.next;
break;
}
temp = temp.next;
}
4.2.2.3 单链表的有序插入的具体实现
对插入的结点进行排序的几种情况:
- 新插入的结点的大小正好处于最后;(temp.next==null)
- 找到了第一个比新节点的大的结点(temp.next.id > node.id),立马跳出循环插入到temp的后面;
- 找到的结点的值跟新结点的一样,如果不允许重复,此方法在这里就应该跳出并结束了
- 举个🌰: 按照年龄非递减顺序插入结点, 姓名相同的跳过插入
//顺序插入链表
public void addByOrder(Node node){
//顺序插入遍历找插入位置的三种情况
/**
* 1. 相等, 跳过这次插入
* 2. temp.next > new node, 把new node插入到temp后面
* 3. temp.next < new node, 继续向后查找
* 4. 没找到比新节点大的, 遍历到末尾了, 把新节点插入到末尾
*/
Node temp = head;
boolean flag = false;
while(true){
if(temp.next == null){
flag = true;
break;
}
//对姓名进行去重
if(temp.next.name == node.name){
break;
}
//只看年龄, 姓名相同的略过
if(temp.next.age > node.age){
flag = true;
break;
}
temp = temp.next;
}
if(flag){
node.next = temp.next;
temp.next = node;
}
}
具体的代码完整实现: Java实现单链表有序插入
4.3 单链表的几个典型例题
4.3.1 求单链表中的有效结点个数
简单; 但是要注意不要统计头结点,我们要的是有效结点
- 直接上代码:
public static int getLength(Node node){
if(head.next == null)
return 0;
int length = 0;
///临时变量
Node temp = head.next;
while(temp != null){
length++;
temp = temp.next;
}
return lenght;
}
4.3.2 查找出单链表中的倒数第k个结点
实现思路:
- 首先单链表只能顺序访问
- 要得到倒数第k个链表, 我们需要知道链表的长度length
- 然后很简单, 直接访问 第length-k个结点(那就是妥妥的倒数第k个结点)就可以了呀
最需要注意的一点: temp=head.next; 我们需要的是有效值的结点,所以需要直接忽略头结点, 避免count次数跟索引对不上.
具体的代码完整实现: Java实现获取单链表的倒数第k个结点
4.3.3 单链表的反转(方法之一, 头插法反转单链表)
实现思路:
- 首先我们定义出需要的结点和指针域,具体包括(标识链表开头的反转链表头结点
reverseHead
, 遍历原链表的结点cur
, 存储cur需要的遍历的下一个节点指针域的cur_next
)- 从头到尾遍历原来的链表.每遍历一个结点,就将其取出, 并放在新的链表 reverseHead的最前端;
- 原来链表的head.next = reverseHead.next;
Q: 为什么 cur_next(存储原链表中cur指向结点的下一个结点的位置)不可或缺?
我们知道cur的作用是遍历原来的链表,每取到一个结点就要插入到反转链表的头结点后(头插法), 然而在把这个结点的引用改成反转链表的引用时, 我们会对cur结点的next指针域做出一定的改变,为了防止这个next指针域丢失, 所以我们一定需要一个cur_next变量去存储cur 的下一个节点的引用.说的再多不如下面代码直观
- 头插法反转链表的主要代码:
///传入原链表头结点的目的是得到原链表的起始位置;
public void reverse(Node head){
///定义需要的Node变量;
Node reverseHead = new Node("");//定义反转链表的头结点
Node cur; //当前结点的指示器, 类似于之前我们经常用的temp哦, 主要目的是遍历原链表的结点,并把每个结点的引用传给反转链表
Node cur_next;// 存储cur的next指针域(下一个结点的引用!)
cur = head.next;
///
while(cur != null){
cur_next = cur.next; //暂时把cur的下一个节点的引用存储一下
cur.next = reverseHead.next;//序号1的作用
reverseHead.next = cur;//序号2的作用
cur = cur_next;//取回存储的引用, 回到原链表继续遍历结点
}
head.next = reverseHead.next;
}
4.3.4 逆序打印单链表(方式1,反转链表并遍历; 方式2,使用栈; 方式3, 递归(待补充! ))
实现思路:
方式1: 先将单链表进行反转操作,然后再遍历即可,这样做的问题是会
破坏原来的单链表结构
,不建议;
方式2: 遍历链表的结点, 把每个结点压入(Push)到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果;
方式2的主要代码:
///链表的逆序打印
Stack<Integer> stack = new Stack<Integer>();
///遍历链表,得到的每个结点都压入到栈中
public void reverseList(){
///临时变量
Node temp = head;
while(true){
if(temp.next == null)
break;
temp = temp.next;
// stack.add(temp.num); ///get方法应该要好一些,
///同样的我们也可以直接把node类压进栈,这时候我们要把泛型类型改为Node,
// 并且我们也一定要重写Node类的toString哦
stack.add(temp.getNum());
}
///所有的数据都压入到栈中了,让我们打印输出栈吧(pop)
while ( stack.size() > 0){
System.out.println(stack.pop());
}
4.3.5 合并两个有序的单链表, 合并之后的链表依然有序
4.3.5.1 尾插法
思路:
- 首先我们需要在结点添加到两个链表的时候进行排序(使用 addByOrder(), 前面小节有过总结);
- 链表1 和 链表2 中的结点都是有序的,这时候我们只需设置一个 mergeList(head_1, head_2),把两个链表的头结点传进去;
- 两个链表在正式进行比较前可能有一下几种情况:
3.1 两个链表都是空的, 那我们直接返回null即可;
3.2 两个链表中有一个为空, 那我们直接返回不空的那个链表的头结点即可;
- 判断完链表的情况后,我们需要初始化两个链表的临时变量temp_1, temp_2, 以对他们进行分别的遍历; 我们还需要设置合并链表的头结点head_3(可以偷懒,直接使用两个链表中的任意一个都行,真的噢! ), 以及合并链表的临时变量 temp_3;
- 接下来我们就来到了激动人心的链表结点比较和遍历时刻, 主要原理就是尾插法, 把两条链表中的正在遍历的两个结点中较小的那个插入到合并链表的后面, 比如下图:
当然了, 如果我们不允许重复的话,就直接同时向后移动 temp_1, temp_2即可;
- 我们的遍历和比较是在while(true)循环中进行的, 那么何时跳出循环呢? 当然是有一个链表遍历完了的情况, 在这里我们需要注意这个判断的写法哦!
- 到了最后一步就比较简单了, 把剩下的一条不为空的链表直接连接到合并链表的尾部就可以了!
查看源代码: 完整代码实例: Java实现合并两个有序的单链表