链表青铜挑战
链表是一种最基本的结构,普通的单链表就是只给你一个指向链表头的指针head,如果想访问其他元素,就只能从head开始一个个向后找,遍历链表最终会在访问尾结点之后如果继续访问,就会返回null。
1. 单链表的构造
首先看一下什么是链表?单向链表就像一个铁链一样,元素之间相互连接,包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个元素的next指向null。如下图:
1.1 先来构造一个单链表
val 表示 当前元素的值,next 是一个 ListNode 类型,指向下一个节点,像一根链子一样。
public class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
next = null;
}
}
1.2 遍历链表
获取链表长度
思路:从头到尾开始遍历,遍历传来的参数 head,循环访问它的下一个节点,如果有值,计数器 +1,如果为空,返回计数器
/**
* 遍历链表,返回 长度
* @param head
* @return
*/
public static int getListLength(ListNode head) {
// 1. 判断链表如果为空,还有下一个为空,返回 0 或 1
if (head == null) {
return 0;
}
if (head.next == null) {
return 1;
}
int length = 1;
while (head.next != null) {
head = head.next;
length++;
}
return length;
}
打印出链表中每个节点的值
跟上面步骤差不多,循环输出每个节点的值,直到下一个为空
/**
* 打印出链表中所有的值
* @param node
*/
public static void printListNode(ListNode node) {
while (node != null) {
System.out.print(node.val + "-->");
node = node.next;
}
System.out.println("NULL");
}
测试
@Test
public void testGetListLength() {
ListNode listNode1 = new ListNode(1);
ListNode listNode2 = new ListNode(2);
ListNode listNode3 = new ListNode(3);
listNode1.next = listNode2;
listNode2.next = listNode3;
System.out.println(listNode1);
System.out.println("链表长度为:" + ListNode.getListLength(listNode1));
ListNode.printListNode(listNode1);
}
1.3 链表插入
注意:单链表的插入有三种方式,头部、中部、尾部
(1)头部插入
需要申请一个元素,将 链表的表头插入一个新数据,之前的表头的位置变成第二位。
新增一个
newNode
节点,保存要插入的节点,newNode
节点的next
值 指向 之前的node
,然后返回
/**
* 头插
* @param val
* @param node
* @return
*/
public static ListNode addFirst(int val, ListNode node) {
ListNode newNode = new ListNode(val);
newNode.next = node;
node = newNode;
return node;
}
(2)尾部插入
跟上面一样,需要申请一个新的节点,将
node
节点遍历到尾部,如果发现下一个元素是null
,那么就这个位置就插入新的节点
/**
* 尾插
* @param val
* @param node
* @return
*/
public static ListNode addEnd(int val,ListNode node) {
ListNode newNode = new ListNode(val);
ListNode head = node;
while (node.next != null) {
node = node.next;
}
node.next = newNode;
return head;
}
错误注意:
如果最后面返回结果是
node
的话,只会返回最后两个节点,因为在循环结束后,node
的指针已经指向了最后一个节点,然后将新节点添加为最后一个节点的下一个节点。最后,你返回的是node
指针,而不是链表的头节点。因此,要在开始前定义一个节点,记录
node
,最后返回这个节点
(3)中间插入
思路:
先对 当前要插入的位置进行判断,如果不符合则报错,为什么这里要
position > length+1
,因为可能我要插入到最后一个元素,比如说一个链表1->2->3->4
我想插入第五个,就是插入在 4 的后面变成1->2->3->4->5
,但是如果插入的为 4,等于链表的长度,链表变成1->2->4->3
,所以说判断位置要+1
如果当前要插入的位置为
1
,或者是 最后position == length+1
,分别调用之前的头插法
和尾插法
就行了定义一个
head
节点,用来保存原来node
的数据定义一个
index计数器进行
遍历,从 2 开始,进行计数,循环遍历当前链表,如果该节点位置与要插入的位置相同,那么则直接跳出循环,在后面进行 节点交换(如果都写在 while 循环里,看起来太复杂了,到到这里反正是会遍历到这个 位置的,所以直接 break)执行完循环后,就已经确定了要插入的位置,直接新建一个
newNode
节点,使这个节点的newNode.next
=node.next
(先前节点的后部分),然后 先前节点前面部分node.next
=newNode
第 5 步有点不好理解,我画个图
代码:
/**
* 中间插入
* @param val
* @param node
* @param position
* @return
*/
public static ListNode addMiddle(int val,ListNode node,int position) {
// 1. 判断位置是否符合
int length = ListNode.getListLength(node);
if (position > length+1 || position < 0) {
System.out.println("总长度: " + length + "插入的位置不合法!" + position);
}
// 判断头插,是否插入在最前
if (position == 1) {
node = ListNode.addFirst(val,node);
return node;
}
// 判断尾插,是否插入在最后
if (position == length+1) {
node = ListNode.addEnd(val,node);
return node;
}
ListNode head = node;
// 2. 循环将位置找出来,一旦符合则插入
int index = 2;
while (node != null) {
// 2.1 如果位置符合,则可以跳出循环去执行了
if (index == position) {
break;
}
node = node.next;
index++;
}
// 现在就是在链表中间了
ListNode newNode = new ListNode(val);
newNode.next = node.next;
node.next = newNode;
return head;
}
1.4 链表删除
(1)头删
直接将
表头
指向下一个即可
/**
* 头删
* @param node
* @return
*/
public static ListNode deleteHead(ListNode node) {
if (node == null) {
return null;
}
return node.next;
}
(2)尾删
删除链表的最后一个,需要找到尾部,但是不需要找到最后一个,而是要找到倒数第二个,然后让它的 next = null,
node 与 head 指向的是同一块区域,任意修改它们的值都会发生改变,返回的是头节点
/**
* 尾删
* @param node
* @return
*/
public static ListNode deleteEnd(ListNode node) {
if (node == null) {
return null;
}
ListNode head = node;
// 当下一个为空时停止
while (head.next.next != null) {
head = head.next;
}
head.next = null;
return node;
}
(3)中间删
和之前的 增加中间节点类似,需要找到目标的前一个节点,找到的话 让 头节点的 node.next = node.next.next;
分析具体思路:
index = 2 ,因为之前已经判断过 index == 1了,是否满足头插,这里下标是从 2 开始找
如果下一个 != null 如果 index == position ,表示已经找到了要插入的位置,跳出循环,执行后面的语句,不符合则后移
跳出循环后,位置肯定找到了,前面是做了判断,position 不符直接报错,
node.next = node.next.next;
这段代码意思就是 当前节点的下一位 => 当前节点的下一位的下一位(有点抽象,看图)// 现在删除的节点为中间 int index = 2; while (node.next != null) { if (index == position) { // 找到位置,跳出循环 break; } index++; node = node.next; } // 找到位置了,跳出循环 node.next = node.next.next; return head;
代码:
/**
* 中间删
* @param node
* @param position
* @return
*/
public static ListNode deleteMiddle(ListNode node,int position) {
if (node == null) {
return null;
}
int length = ListNode.getListLength(node);
if (position > length || position <= 0) {
System.out.println("删除位置不正确 " + position);
return null;
}
// 判断是否满足 头插、尾插
if (position == 1) {
return ListNode.deleteHead(node);
}
if (position == length) {
return ListNode.deleteEnd(node);
}
ListNode head = node;
// 现在删除的节点为中间
int index = 2;
while (node.next != null) {
if (index == position) { // 找到位置,跳出循环
break;
}
index++;
node = node.next;
}
// 找到位置了,跳出循环
node.next = node.next.next;
return head;
}
2. 双向链表
双向链表顾名思义就是既可以向前,也可以向后。有两个指针的好处自然是移动元素更方便。该结构我们在工程里有大量的应用
其中双向由
prev
引用保证,而循环则是头尾节点的相互指向实现的。
2.1 构造双向链表
初始化
public class DoubleNode {
public int data; // 数据
public DoubleNode next; // 后一个节点
public DoubleNode pre; // 前一个节点
public DoubleNode(int data) {
this.data = data;
}
/**
* 打印所有节点信息
* @param node
*/
public static void printDoubleNode(DoubleNode node) {
if (node == null) {
System.out.println("双向链表为空");
return;
}
while (node.next != null) {
System.out.print(node.data + "->");
node = node.next;
}
System.out.print("null");
}
}
测试
// 测试双向链表
@Test
public void testDoubleNode() {
// 初始化双向链表
DoubleNode doubleNode1 = new DoubleNode(2);
DoubleNode doubleNode2 = new DoubleNode(4);
DoubleNode doubleNode3 = new DoubleNode(6);
DoubleNode doubleNode4 = new DoubleNode(8);
doubleNode1.next = doubleNode2;
doubleNode2.pre = doubleNode1;
doubleNode2.next = doubleNode3;
doubleNode3.pre =doubleNode2;
doubleNode3.next =doubleNode4;
doubleNode4.pre = doubleNode3;
DoubeNode.printDoubleNode(doubleNode1);
}
2.2 双向链表插入
(1)头插
在链表头插入,将头节点的上一个节点指向 新节点
node.pre = newNode;
再将新节点的下一个节点指向头节点
newNode.next = node;
代码:
/**
* 头插
* @param node
* @return
*/
public static DoubleNode addHead(int val,DoubleNode node) {
DoubleNode newNode = new DoubleNode(val);
if (node == null) {
return newNode;
}
node.pre = newNode;
newNode.next = node;
return newNode;
}
(2)尾插
思路:在双向链表最后插入数据,先定义一个
node
变量保存head
信息,用于返回
先循环遍历到最后,使
head
指向最后一个节点然后
head.next
指向新节点newNode
,然后newNode.pre
指向head
/**
* 尾插
* @param val
* @param head
* @return
*/
public static DoubleNode addEnd(int val,DoubleNode head) {
DoubleNode newNode = new DoubleNode(val);
if (head == null) {
return newNode;
}
DoubleNode node = head;
// 需要找到尾节点,遍历到最后一个
while (head.next != null) {
head = head.next;
}
// 此时已经到了最后
head.next=newNode;
newNode.pre = head;
return node;
}
3)中间插
思路 还是跟上面中间插入单链表差不多,只不过是找到之后 还要 指定
pre
节点请注意看这一块核心代码:
从
index = 2
开始找,因为如果是 1 的话就是头插法了,更关键的是可以提前判断位置
,看图吧int index = 2; while (head.next != null) { if (index == position) { newNode.next = head.next; head.next = newNode; newNode.pre = head; } head = head.next; index++; }
代码:
/**
* 中间插入
* @param val
* @param head
* @param position
* @return
*/
public static DoubleNode addMiddle(int val,DoubleNode head,int position) {
DoubleNode newNode = new DoubleNode(val);
if (head == null) {
return newNode;
}
// 获取长度,判断是否符合
int length = DoubleNode.getLength(head);
if (position > length+1 || position <= 0) {
System.out.println("需要添加的位置不符合条件");
return null;
}
// 判断是否可以 头插、尾插
if (position == 1) {
return DoubleNode.addHead(val,head);
}
if (position == length+1) {
return DoubleNode.addEnd(val,head);
}
// 现在就是往中间插
DoubleNode node = head;
int index = 2;
while (head.next != null) {
if (index == position) {
newNode.next = head.next;
head.next = newNode;
newNode.pre = head;
}
head = head.next;
index++;
}
return node;
}
2.3 双向链表删除
(1)头删
头节点的指向 next,然后 使 pre 值为空,因为当前位置已经是头节点了
/**
* 删除头节点
* @param head
* @return
*/
public static DoubleNode deleteHead(DoubleNode head) {
if (head == null) {
return null;
}
head = head.next;
head.pre = null;
return head;
}
(2)尾删
把链表头遍历到倒数第二个,因为最后一个要删除,使 倒数第二个 .next = null
/**
* 删除尾节点
* @param head
* @return
*/
public static DoubleNode deleteEnd(DoubleNode head) {
if (head == null) {
return null;
}
DoubleNode node = head;
// 需要把节点遍历到最后,遍历到倒数第二个,因为最后一个要删除
while (head.next.next != null) {
head = head.next;
}
head.next = null;
return node;
}
(3)中间删
大致思路跟上面 单链表 中间删一致,只不过是多加了几步判定条件
先判断是否为空,判断删除的下标是否满足,判断是否满足头插、尾插
从下标 2 开始找,为什么不是1?如果是1的话那么就是满足上面的头删,使 head 节点也后面移动一位
循环遍历到到删除的节点上
当前元素的 上一个元素指向的下一个元素 = 当前元素的下一个元素
当前元素的 下一个元素的 pre(上一个元素) = 当前元素的 上一个
if (index == position) { head.pre.next = head.next; head.next.pre = head.pre; }
代码:
/**
* 删除中间节点
* @param head
* @param position
* @return
*/
public static DoubleNode deleteMiddle(DoubleNode head,int position) {
if (head == null) {
return null;
}
// 获取长度,并且判断是否符合
int length = DoubleNode.getLength(head);
if (position > length || position <= 0) {
System.out.println("删除的位置错误!");
return null;
}
// 判断是否满足头删、尾删
if (position == 1) {
return DoubleNode.deleteHead(head);
}
if (position == length) {
return DoubleNode.deleteEnd(head);
}
DoubleNode node = head;
/**
* 从下标 2 开始找,为什么不是1?如果是1的话那么就是满足上面的头删
* 使 head 节点也后面移动
*/
int index = 2;
head = head.next;
while (head.next != null) {
/**
* 如果当前的位置是要删除的位置
* 1.当前元素的 上一个元素指向的下一个元素 = 当前元素的下一个元素
* 2.当前元素的 下一个元素的 pre(上一个元素) = 当前元素的 上一个
*/
if (index == position) {
head.pre.next = head.next;
head.next.pre = head.pre;
}
index++;
head = head.next;
}
return node;
}