本文已参与「新人创作礼」活动,一起开启掘金创作之路。
前言
本章详细阐述了链表这一种线性数据结构的使用方法以及相关操作。 其实很多人学链表不能理解它节点与节点之前的链接方式学起来就会很快困难,因为我当初其实也是这样。后面自己想通了,其实就是下面的思想: 比如“temp.next”,这个有时候表示一个节点,有时候表示一个指针,只要区分开这个概念,并带入到代码当中去理解,就很好懂的。
单链表
如图所示,链表是由一个又一个节点连起来的,而每一个节点又是有一块数据域和一块指针域合起来的。 其中数据域即存放的这个节点的数据,指针域则指向了下一个节点的地址空间。 这一块稍微需要xdm有点基础才行,实在理解不了就多看代码多去找东西学习就完事儿了,慢慢儿就上道了。我这里直接给出实现方法,大家可以去找相关的视频或者书籍进行学习,因为我感觉我用文字将链表这东西肯定没有别人用视频来讲要清楚,而且我的代码里注释都很全,可以参考应该也很好懂。 代码实现:
package 链表;
public class LinkedListTest {
public static void main(String[] args) {
//测试
//创建节点
Node n1 = new Node(5);
Node n2 = new Node(4);
Node n3 = new Node(10);
Node n4 = new Node(6);
//创建单链表
singleLinkedList list = new singleLinkedList();
//添加节点
list.add(n1);
list.add(n2);
list.add(n3);
list.add(n4);
//打印单链表
list.show(); //输出5 4 10 6
//删除功能测试:我们删除5
list.delete(5); //输出4 10 6
list.show();
//插入功能测试,我们在4后面插入88,
//这里的下标和数组的下标值意义是一样的
//即传入的参数应该是index=1,newNode.data=88
Node n5 = new Node(88);
list.insert(1,n5);
list.show(); //输出4 88 10 6
//测试update更新功能,我们更新88为99
list.update(88,99);
list.show(); //输出4 99 10 6
}
}
//定义单链表
class singleLinkedList{
//初始化一个头节点,头节点不要动,因为它是整个链表的头,不存放数据
Node headNode = new Node(0);
//返回头结点方法
public Node getHeadNode(){
return headNode;
}
//--------------------------------------------------------------------
//添加新节点到单链表的方法:add
//思路,当不考虑顺序时
//1、找到当前链表的最后一个节点
//2、将要添加的节点挂载在最后这一个节点上即可
//即让最后一个节点的指针域指向新节点
public void add(Node newNode){
//首先,创建一个工作指针,代替头节点headNode进行遍历,这是为了不改变头结点的位置
//因为头指针拿来遍历的话那么头指针原先存的地址就会改变
Node temp = headNode;
while (true){
if(temp.next == null) break; //当temp遍历到某个节点的next域为null时,说明到尾节点了
//如果没到尾节点,指针后移,继续遍历下一个
temp = temp.next;
}
//当退出循环时,证明已经到了最后一个节点,挂上我们的新节点即可
temp.next = newNode;
}
//--------------------------------------------------------------------
//插入新节点到单链表的方法:insert
//思路:
//1、用一个计数器cnt计算遍历到第几个位置了
//2、从第一个节点开始遍历,当cnt == 我们要插入的位置index时进行插入新节点newNode
public void insert(int index,Node newNode){
Node temp = headNode; //工作指针
int cnt = 0; //计数器
while(true){
if(temp.next == null) break; //遍历结束,退出循环
if(cnt == index){ //寻找到目标位置,进行插入
newNode.next = temp.next;
temp.next = newNode;
break;
}
temp = temp.next; //指针后移
cnt++;
}
}
//--------------------------------------------------------------------
//删除单链表某节点的方法:delete
public void delete(int target){ //target:要删除的元素
Node temp = headNode;
while (true){
//为防止空指针异常,应该把值为null就结束的判断放在第一行
if(temp.next == null) break;
if(temp.next.data == target){ //找到要删除的节点的前一个节点,我们才能删除该节点
temp.next = temp.next.next;
break;
}
temp = temp.next; //指针后移
}
}
//--------------------------------------------------------------------
//更新单链表某节点的方法:update
//srcData:要更新的链表中的目标元素值,newData:用以替换的元素值
public void update(int srcData,int newData){
Node temp = headNode;
while (true){
if(temp.next == null) break; //遍历结束,退出循环
if(temp.data == srcData){ //如果找到了目标元素值
temp.data = newData; //将数据域给替换
break;
}
temp = temp.next; //指针后移
}
}
//--------------------------------------------------------------------
//遍历输出链表的方法:show
public void show(){
System.out.print("单链表数据为:");
Node temp = headNode;
while (true){
if(temp.next == null) break;
temp = temp.next;
System.out.print(temp.data+" ");
}
System.out.println();
}
}
//定义节点
class Node{
public int data; //数据域
//指针域,指向下一个节点的位置,因为下一个节点也是Node类型
//所以这个next的数据类型也为Node
public Node next;
//构造方法中直接声明该节点的数据域
public Node(int data){
this.data = data;
}
//打印节点信息
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
}
链表实现栈数据结构
栈这种数据结构我在数组那一章里面已经说过了,这里不再赘述,一样直接上源码: 代码实现:
package 链表;
public class LinkedStack {
public static void main(String[] args) {
//测试
//创建节点
Node n1 = new Node(5);
Node n2 = new Node(4);
Node n3 = new Node(10);
Node n4 = new Node(6);
//创建栈
linkedListStack stack = new linkedListStack();
//测试入栈功能
stack.push(n1);
stack.show(); //输出5
stack.push(n2);
stack.push(n3);
stack.push(n4);
stack.show(); //输出5 4 10 6
//测试出栈功能
stack.pop();
stack.show(); //输出5 4 10
stack.pop();
stack.pop();
stack.show(); //输出5
//再入栈
stack.push(n2);
stack.push(n3);
stack.show(); //输出5 4 10
}
}
//定义栈
class linkedListStack {
//定义头结点
Node headNode = new Node(0);
//返回头结点
public Node getHeadNode() {
return headNode;
}
//--------------------------------------------------------
//入栈方法push
public void push(Node newNode) {
//工作指针
Node temp = headNode;
while (true) {
if (temp.next == null) break;
temp = temp.next;
}
temp.next = newNode;
}
//--------------------------------------------------------
//出栈方法pop
public int pop() {
Node temp = headNode;
while (true) {
if (temp.next.next == null) break;
temp = temp.next;
}
//退出循环时,temp位于链表的倒数第二个节点的位置
int element = temp.next.data; //取出最后一个节点的值,一会儿要返回
//弹出最后一个节点,就是删除最后一个节点
temp.next = null;
return element;
}
//--------------------------------------------------------
//遍历栈
public void show(){
Node temp = headNode;
System.out.print("栈数据为:");
while (true){
if(temp.next == null) break;
temp = temp.next;
System.out.print(temp.data + " ");
}
System.out.println();
}
}
//定义节点
class Node{
public int data; //数据域
//指针域,指向下一个节点的位置,因为下一个节点也是Node类型
//所以这个
public Node next;
//构造方法中直接声明一个节点
public Node(int data){
this.data = data;
}
//打印节点信息
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
}
链表实现队列数据结构
关于队列在数组那一章节同样有所介绍,这里直接给出源码。 代码实现:
package 链表;
public class LinkedListQueue {
public static void main(String[] args) {
//测试
//创建节点
Node n1 = new Node(5);
Node n2 = new Node(4);
Node n3 = new Node(10);
Node n4 = new Node(6);
//创建队列
LinkedQueue queue = new LinkedQueue();
//测试入队列
queue.put(n1);
queue.show(); //输出5
queue.put(n2);
queue.put(n3);
queue.put(n4);
queue.show(); //输出5 4 10 6
//测试出队列
queue.out();
queue.show(); //输出4 10 6
queue.out();
queue.show(); //输出10 6
}
}
class LinkedQueue{
//定义头结点
Node headNode = new Node(0);
//返回头结点
public Node getHeadNode(){ return headNode; }
//---------------------------------------------
//入队列
public void put(Node newNode){
Node temp = headNode;
while (true){
if(temp.next == null) break;
temp = temp.next;
}
temp.next = newNode;
}
//---------------------------------------------
//出队列
public int out(){
//队列因为是一段进,一段出,因为我们添加的时候是在队尾添加的
//所以这里出队列我们应该是在队头出,所以我们直接用头节点操作就行
//不需要工作指针
int element = headNode.next.data; //将队头的元素取出,一会儿要返回
headNode = headNode.next;
return element;
}
//---------------------------------------------
//遍历队列
public void show(){
Node temp = headNode;
System.out.print("队列数据为:");
while (true){
if(temp.next == null) break;
temp = temp.next;
System.out.print(temp.data+" ");
}
System.out.println();
}
}
//定义节点
class Node{
public int data; //数据域
//指针域,指向下一个节点的位置,因为下一个节点也是Node类型
//所以这个
public Node next;
//构造方法中直接声明一个节点
public Node(int data){
this.data = data;
}
//打印节点信息
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
}
实现双向链表
为什么会有双向链表? 因为单链表存在的问题:
1、单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。 2、单向链表不能自我删除,需要靠辅助节点,,而双向链表则可以自我删除,所以我们前面的单链表删除节点时,总是找到temp,temp是待删除节点的前一个节点,我们通过它来删除我们要删除的节点。
双向链表其实也不复杂,就是比原来的单链表要多一个前驱指针pre。
双向链表的增删改查的思路如下:
代码实现:
package 链表;
public class DoubleLinked {
public static void main(String[] args) {
//测试
//创建节点
Node n1 = new Node(5);
Node n2 = new Node(4);
Node n3 = new Node(10);
Node n4 = new Node(6);
//创建双向链表
doubleLinkedList list = new doubleLinkedList();
//添加功能的测试
list.add(n1);
list.show(); //输出5
list.add(n2);
list.add(n3);
list.add(n4);
list.show(); //输出5 4 10 6,6 4 10 5
//删除功能的测试: 我们删除5
list.delete(5);
list.show(); //输出4 10 6,6 4 10
//更新方法的测试:我们更新4为88
list.update(4,88);
list.show();//输出 88 10 6,6 10 88
//插入方法的测试:我们在88后面插入99,即在第二个位置插入99
//即index=2,newNode.data=99
Node n5 = new Node(99);
list.insert(2,n5);
list.show(); //输出99 88 10 6,6 10 88 99
}
}
//定义双向链表
class doubleLinkedList{
//定义头结点
Node headNode = new Node(0);
//返回头节点
public Node getHeadNode(){ return headNode; }
//-----------------------------------------------
//添加节点
//没啥特别的,默认添加在最后就行
public void add(Node newNode){
Node temp = headNode;
while (true){
if(temp.next == null) break;
temp = temp.next;
}
temp.next = newNode;
newNode.pre = temp;
}
//-------------------------------------------------
//删除delete
public void delete(int target){ //target删除的目标值
Node temp = headNode;
boolean flag = false; //标志删除的是否是最后一个节点
//如果删除的是最后一个节点,那么我们应该改变操作
while (true){
if(temp.next == null) {
flag = true; //删除的是最后一个节点
break;
}
if(temp.data == target){
//temp的下一个节点的前驱指针指向temp节点的前一个节点
temp.next.pre = temp.pre;
//temp的前一个节点的后继指针指向temp节点的后一个节点
temp.pre.next = temp.next;
break; //退出循环
}
temp = temp.next; //指针后移
}
if(flag == true){
//因为删除的是最后一个节点,且temp此时代表是也是最后一个节点
//那么我们先将temp的前一个节点的后继指针置为空
temp.pre.next = null;
//再把temp的前驱指针置为空就行
temp.pre= null;
}
}
//---------------------------------------------
//插入元素:insert
//index:插入的位置。newNode:插入的新节点
public void insert(int index,Node newNode){
Node temp = headNode;
int cnt = 0; //计数器,计数遍历到了第几个节点位置
while (true){
if(temp == null) break;
if(cnt == index){ //找到插入位置
//temp的前一个节点的后继指针指向新节点
temp.pre.next = newNode;
//新节点的前驱指针指向temp 的前一个节点
newNode.pre = temp.pre;
//temp的前驱指针指向新节点
temp.pre = newNode;
//新节点的后继指针指向temp
newNode.next = temp;
break;
}
temp = temp.next;
cnt++;
}
}
//---------------------------------------------
//更新方法update
//srcData,要更新的目标元素,targetData:用以替换的数据元素
public void update(int srcData,int targetData){
Node temp = headNode;
while (true){
if(temp == null) break;
if(temp.data == srcData){
temp.data = targetData;
break;
}
temp = temp.next;
}
}
//---------------------------------------------
//遍历队列
public void show(){
Node temp = headNode;
System.out.print("向后遍历数据为:");
while (true){
if(temp.next == null) break;
//指针先后移再输出值是为了避免出现头结点中的data值
//因为我们默认头结点是不放值的
temp = temp.next;
System.out.print(temp.data+" ");
}
System.out.println();
System.out.print("向前遍历数据为:");
while (true){
if(temp.pre == null) break;
//这里也是为了避免出现头结点中的data值
//写法不一样是因为这里是从后往前遍历的
System.out.print(temp.data+" ");
temp = temp.pre;
}
System.out.println();
}
}
//定义节点
class Node{
public int data; //数据域
public Node next; //next是指向后面节点的指针
public Node pre; //pre是指向前面节点的指针
//构造方法中直接声明一个节点
public Node(int data){
this.data = data;
}
//打印节点信息
@Override
public String toString() {
return "Node{" +
"data=" + data +
'}';
}
}
环形链表解决约瑟夫环问题
为什么要有单向环形链表?
看一下丢手帕问题:
环形链表的逻辑图:
环形链表也牵扯到非常经典的约瑟夫环的问题:
举个例子:
现在有五个小孩(n = 5),然后从第一个人开始报数(即k = 1),报数报到2时(m = 2),该小孩就出队列。
示意图如下:
一开始有5个小孩,他们呈环形链表的状态:
由题意可知,从第一个人开始报数,报到数字为2的人,即2号出队列,删除该节点后情况如下:
然后现在又从3开始报数,因为它是刚刚出队列的2号后面的第一个人,所以此时的二号节点为第四个人,所以他出队列,该节点被删除,即目前的出队列顺序为2->4,下面的分析逻辑一样,值得注意的是,在删到最后只剩一个节点时,它自己也能形成闭环:
单向环形链表实现约瑟夫问题:
创建环形链表的思路:
出队列顺序方法的思路:
有三个指针变量,一个是first,指向当前环形链表的第一个节点(即第一个人),它不会改变,永远指向第一个人,然后还有两个指针是boy和curBoy。其中curBoy是辅助指针,指向的是我们的当前boy节点(currentBoy),用来帮助构建我们的环形链表,boy指针则是表示我们的每一个boy节点的,代码中有详细的注释,这里描述有些抽象。
package 链表;
public class Josepfu {
public static void main(String[] args) {
//测试
//创建环形链表
CircleSingleLinkedList list = new CircleSingleLinkedList();
//我们创建含有五个小孩的环形链表
list.addBoy(5);
//测试遍历功能
list.showBoy(); //输出1,2,3,4,5
//现在我们测试出圈顺序
list.countBoy(1,2,5);//输出2,4,1,5,3
}
}
//创建环形链表
class CircleSingleLinkedList{
//新建一个first节点,用来表示第一个Boy节点,可以先没有值,因为我们并不确定编号
public Boy first = null;
//-------------------------------------------------------------
//我们提供一个addBoy方法,用来接收这个环到底有多少个节点
//然后创建对应的环形链表
public void addBoy(int n){
Boy curBoy = null; //辅助指针,用来帮助构建环形链表用的
//使用for循环来创建环形链表
for (int i = 1; i <= n; i++) {
//根据编号,每循环一次就创建一个boy节点
Boy boy = new Boy(i);
if(i == 1){ //表示遇到了第一个小孩
//那么我们将first指针指向它,这个first以后就不动了
first = boy;
first.next = first; //自己指向自己,构成环状
curBoy = first; //让curBoy指向第一个小孩
}else{ //说明不是第一个boy节点了
//curBoy指向的是第一个节点first,下面这一句便将第一个节点的next域指向了新的boy节点
curBoy.next = boy;
//新的boy节点的next域应该指向头节点first,构成环路
boy.next = first;
//我们的辅助指针也应该指向第二个节点了,而不在指向第一个节点
curBoy = boy;
//后面的2、3、4...等节点的添加都是这个逻辑
}
}
}
//--------------------------------------------------------
//遍历当前的环形链表
public void showBoy(){
//判断链表是否为空
if(first == null) {
System.out.println("没有小孩嗷");
return;
}
//因为first头指针不能动,所以我们仍然使用一个辅助指针进行遍历
Boy curBoy = first;
while (true){
System.out.printf("小孩的编号为 %d \n",curBoy.no);
if(curBoy.next == first) { //说明又遍历到头部了,遍历结束
break;
}
curBoy = curBoy.next; //curBoy指针后移
}
}
//-------------------------------------------------------
//根据用户的输入,计算出小孩出圈的顺序
//startNo:表示从第几个小孩开始数数
//countNum:表示数几下
//nums:表示圈中有多少个小孩在圈中
public void countBoy(int startNo,int countNum,int nums){
//首先创建一个辅助遍历helper
Boy helper = first;
//让其指向链表中的最后一个节点
while (true){
if(helper.next == first) break; //遍历到了最后一个节点,退出循环
helper = helper.next;
}
//小孩报数前,先让first和helper移动startNo-1次,减一次是因为两点之间只要移动一次就行
//意思是让这两个指针先做好准备移动前的准备,因为first指向的一定是第一个先报数的小孩
//注意画图理解,不然会有一点抽象
for (int i = 0; i < startNo-1; i++) {
first = first.next;
helper = helper.next;
}
//循环出圈,直到最后圈中只剩一个节点
while (true){
if(helper == first) break; //说明圈中只剩最后一个节点了,那么退出循环
//当小孩报数时,让helper和first指针同时移动countNum-1次
//即报多少数移动多少次
for (int i = 0; i < countNum-1; i++) {
first = first.next;
helper = helper.next;
}
//移动之后,说明报数完毕,那么将first指向的那个boy节点出圈
System.out.printf("编号为 %d 的小孩出圈 \n",first.no);
//这时将first指向它的下一个节点,让first当前指向的这个节点出圈
first = first.next;
helper.next = first;
//原来的first指向的节点没有任何引用,就会被GC回收
}
//现在将最后一个小孩出圈即可完成所有的出圈操作
System.out.printf("最后出圈的小孩的编号为:%d \n",first.no);
}
}
//创建一个Boy类,表示一个节点
class Boy{
public int no; //编号,可以理解为数据域
public Boy next; //指针域,指向下一个节点
//构造方法,new Boy实例时就给该节点赋上数据域的值
public Boy(int no){
this.no = no;
}
//打印节点信息
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}