LeetCode | 链表基本操作
https://github.com/dolphinmind/datastructure/tree/datastructure-linkedlist
203 移除链表元素 | 707 设计链表 | 206 反转链表 | 24 两两交换链表中的节点
分析
理解链表
理解链表,我觉得最需要区分的概念就是什么是数据元素,什么是容器,而容器就是用于装配数据元素使用,容器的排列让散列的数据元素产生了秩序。
近几天的思考逐渐发觉,数组和链表尽管是以存储方式和某些操作进行区分,可本质上却是一致的。
数组和链表内部都存在一系列的装配数据元素的基本容器,int[] nums 中的基本容器 可看做int
,SingleLinkedList 中的基本容器为ListNode
,仔细分析发现,ListNode中的next其实对于数组而言也同样存在,只是链表需要自己主动指定,数组则是内定,遍历同样都可以从起始开始遍历到边界。
链表和数组我从任意位置进行拆分仍然是链表或数组,都具有递归性质
虚拟头结点
循环不变量
的概念,从数组中的二分法开始就始终贯穿,主要还是在处理边界上,不对边界进行孤立处理。- 链表并不需要掌控其中所有结点的准确位置,只有精确地知道头节点的位置,这条链表就可以完全获得
- 虚拟头结点引入之后,原始的头结点就可以退化为普通结点,进行同等操作,而不必肩负起扛起整个链表的重任
- 虚拟头结点与前缀和数组中对原始数组进行padding操作有异曲同工之妙,尽管只是在边界增加了一个基本容器,却让编码的语义更具备人类可读性
链表的操作
链表的增加结点,跟链表的删改查不同,前者考虑的是所有结点之间的空隙,n=[0,length],后者只考虑结点本身[1,length],可否将两者进行整合呢?
如何看待链表增加操作中输入的n?n的取值范围是什么?n的具体位置表示什么含义?
-
n = [0,length+1],这里面的n表示的是间隙;如果用结点序数来看 n = 1, length+1;我当前只关注前面的情况。
-
不引入虚拟头节点的情况,我将其命名为
insertNodeBeforeHead
,类比数组的下标索引从0开始,链表真实头节点可看做索引为0 -
这种情况下 , n = 0 表示在链表头部插入一个结点, n = length表示在链表尾部插入一个结点,那么 n = 10呢?表示在索引为10,序数为第11个元素之前插入一个结点
-
引入虚拟头节点的情况,我将其命名为
insertNodeAfterDummyHead
,那么虚拟头节点索引可看做为0,后面的结点索引不就是对应正常的序数? -
在这种情况下,n = 0 就是在虚拟头节点后面插入一个数据,n = 1,就是在 序数为1的头节点之后插入一个数据, n = 10, 表示在序数为10的结点后面插入一个数据
链表拆分
插入一个结点,会破坏原有的链表。将链表一分为二,左侧的链表有头结点照料着,右侧的子链表如果没有可指示的临时头结点,就成为垃圾内存了,所以在操作的时候,先保证所有链表(包括处理的子链表)都有头结点,能被访问,然后再进行操作。LeetCode 24其实就是这种思路
主类
ListNode
package com.github.dolphinmind.linkedlist.uitls;
/**
* @author dolphinmind
* @ClassName ListNode
* @description
* @date 2024/8/3
*/
// 链表组成元素:节点
public class ListNode<E> {
E val;
ListNode<E> next;
public ListNode()
{
this.val = null;
this.next = null;
}
public ListNode(E val, ListNode<E> next)
{
this.val = val;
this.next = next;
}
}
SingleLinkedList
package com.github.dolphinmind.linkedlist.uitls;
/**
* @author dolphinmind
* @ClassName SingleLinkedList
* @description 单链表
* @date 2024/8/3
*/
public class SingleLinkedList<E> {
// 单链表头结点
private ListNode<E> head;
public SingleLinkedList()
{
this.head = null;
}
// 1. 将数组元素转化为链表
public void array2LinkedList(E[] nums) {
if (null == nums || nums.length == 0) {
return ;
}
head = new ListNode<>(nums[0], null);
ListNode<E> cur = head;
for (int i = 1; i < nums.length; i++) {
cur.next = new ListNode<>(nums[i], null);
cur = cur.next;
}
}
// 2. 头部增加结点
public void addNodeHead(ListNode<E> node) {
if (null == node) {
return;
}
System.out.println("头部增加新结点:");
ListNode<E> dummyHead = new ListNode<>();
dummyHead.next = node;
node.next = head;
head = dummyHead.next;
}
// 3. 尾部增加结点
public void addNodeTail(ListNode<E> node) {
if (null == node) {
return;
}
System.out.println("尾部增加新结点:");
ListNode<E> cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
/** 插入结点:包含原链表本身及其两侧的边界扩展
* insertNodeBeforeHead 与 insertNodeAfterDummyHead 等价
* 1. 链表可以看做等价于数组的存在,索引都看做从0开始
* 2. 两种对于n的看法,逻辑边界处理
* insertNodeBeforeHead, n表示以Head为索引0 表示在对应索引之前添加一个结点
* insertNodeAfterDummyHead n表示以DummyHead为索引0,表示在对应索引之后添加一个结点
* 3. cur永远在目标索引之前的位置
* @param node 待插入结点
* @param n n = [0, length)
*/
public void insertNodeBeforeHead(ListNode<E> node, int n) {
// 输入判断
if (null == node || n < 0 || null == head) {
return;
}
// 头部插入结点
if (n == 0) {
node.next = head;
head = node;
return;
}
ListNode<E> cur = head;
// 移动游标至目标的前一个位置,移动的最大次数为n-1
for (int i = 1; i < n; i++) {
if (null != cur.next) {
cur = cur.next;
} else {
System.out.println( "cur游标指针到了原链表末端,n超出了链表边界,末端直接添加结点");
break;
}
}
// 即便cur.next = null, 对后续也没有影响,相当于插入到尾部
node.next = cur.next;
cur.next = node;
}
/**
* 添加入虚拟头节点,使得每个结点的处理条件都一致,与二分法的逻辑区间划分逻辑上是相通的,保证了循环不变量
* @param node
* @param n
*/
public void insertNodeAfterDummyHead(ListNode<E> node, int n) {
if (null == node || n < 0) {
return;
}
ListNode<E> dummyHead = new ListNode(null, head);
ListNode<E> cur = dummyHead;
// 移动游标至目标的前一个位置,移动的最大次数为n
for (int i = 0; i < n; i++) {
if (null != cur.next) {
cur = cur.next;
} else {
System.out.println( "cur游标指针到了原链表末端,n超出了链表边界,末端直接添加结点");
break;
}
}
node.next = cur.next;
cur.next = node;
head = dummyHead.next;
}
/**
* 删除结点直包含原有的链表结点本身
* @param n
*/
public void deleteNode(int n) {
if (n < 1 || head == null) {
return;
}
ListNode<E> dummyHead = new ListNode(null, head);
ListNode<E> cur = dummyHead;
// 移动游标至目标的前一个位置,移动的最大次数为n
for (int i = 1; i < n; i++) {
if (null != cur) {
cur = cur.next;
} else {
System.out.println( "cur游标指针到了原链表末端,n超出了链表边界,删除结点失败!");
return;
}
}
cur.next = cur.next.next;
head = dummyHead.next;
}
/**
* 查询/修改一致,只对原有的链表进行操作
* 加入虚拟结点,只是为了统一处理
* @param n
*/
public void searchNode(int n) {
if (n < 1 || null == head) {
return;
}
ListNode<E> dummyHead = new ListNode(null, head);
ListNode<E> cur = dummyHead;
for (int i = 0; i < n; i++) {
if (null != cur.next) {
cur = cur.next;
} else {
System.out.println( "cur游标指针到了原链表末端,n超出了链表边界,查询结点失败!");
return;
}
}
System.out.println(cur.val);
}
/**
* 链表翻转
*/
public void reverseSingleLinkedList() {
// 头节点为空,或者为单个结点直接返回
if (null == head || head.next == null) {
return;
}
// 左侧链表头部
ListNode<E> leftHead = null;
// 拆分链表获取原链表左侧第一个结点
ListNode<E> rightSlow = head;
// 拆分链表获取链表右侧
ListNode<E> rightFast = head.next;
// 外部循环控制rightSlow边界至链表末尾
while (null != rightSlow) {
// 左侧链表
rightSlow.next = leftHead;
leftHead = rightSlow;
// 右侧链表
rightSlow = rightFast;
// 内部判断控制rightFast边界null
if (null != rightFast) {
rightFast = rightFast.next;
}
}
head = leftHead;
}
/**
* 链表两两结点交换 LeetCode 24
* cursor引领左侧子链表
* right 引领右侧子链表
*/
public void swapPairs() {
ListNode<E> dummyHead = new ListNode<>(null, head);
ListNode<E> cursor = dummyHead;
while (null != cursor.next && null != cursor.next.next) {
ListNode<E> left = cursor.next;
ListNode<E> mid = cursor.next.next;
ListNode<E> right = cursor.next.next.next;
cursor.next = mid;
mid.next = left;
left.next = right;
cursor = left;
}
head = dummyHead.next;
}
// 打印链表
public void printLinkedList() {
if (null == head) {
return;
}
ListNode<E> cur = head;
while (null != cur) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
}
package com.github.dolphinmind.linkedlist;
import com.github.dolphinmind.linkedlist.uitls.ListNode;
import com.github.dolphinmind.linkedlist.uitls.SingleLinkedList;
import org.junit.Test;
import java.util.Arrays;
public class SingleLinkedListTest {
/**
* <p>重点:
* Java 泛型确实主要设计用于处理引用类型,这意味这泛型通常与包装类一起使用。这是因为Java泛型在编译时需要类型信息来进行
* 类型检查,而在运行时使用类型擦除,这意味着泛型的实际类型参数会被替换为它们的基类型(对于引用类型来说通常是Object或
* 特定的接口)。因此,基本类型如int, char, boolean等不能使用直接作为泛型参数使用,因为它们没有对应的运行时类型信息
*</p>
*
* <p>泛型与基本类型
* 基本类型:如int, double,char等,这些类型在内存中占用固定戴奥的空间,并且存储在栈中
* 包装类:如Integer,Double, Character等,这些类是基本类型的对应对象形式,它们继承自java.lang.Object类,并且
* 可以作为泛型参数使用
* <p/>
* <p>
* 泛型与包装类
* 当使用泛型时,实际上是在告诉编译器:"我将使用某种类型的对象,但我不关心它是什么类型。" 这样可以编写可重用代码
* 由于基本类型不是对象,它们不能直接作为泛型参数使用。但是Java提供了自动装箱和拆箱机制,使得在大多数情况下,我们可以像使用对象意义使用基本类型
* </p>
*
*<p> 自动装箱和拆箱
* 自动装箱:将基本类型自动转换为相应的包装类对象
* 自动拆箱:将包装类对象自动转换为基本类型
*</p>
*
* @description 测试数组转换为链表
*/
public SingleLinkedList<Integer> init() {
int[] nums = {1, 2, 3, 4, 5, 6, 7, 8};
if (nums instanceof int[]) {
// System.out.println("nums is int[]");
}
// System.out.println("数组元素中的基本类型判断" + nums.getClass().getComponentType().getName());
// 1. 创建 SingleLinkedList 对象
SingleLinkedList<Integer> singleLinkedList = new SingleLinkedList<>();
// 2. 使用自动装箱将 int[] 转换为 Integer[]
Integer[] boxedNums = Arrays.stream(nums).boxed().toArray(Integer[]::new);
// 调用 array2LinkedList 方法
singleLinkedList.array2LinkedList(boxedNums);
singleLinkedList.printLinkedList();
return singleLinkedList;
}
@Test
public void test_array2LinkedList() {
init().printLinkedList();
}
@Test
public void test_addNodeHead() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.addNodeHead(new ListNode<>(10, null));
singleLinkedList.printLinkedList();
singleLinkedList.addNodeHead(new ListNode<>(11, null));
singleLinkedList.printLinkedList();
}
@Test
public void test_addNodeTail() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.addNodeTail(new ListNode<>(10, null));
singleLinkedList.printLinkedList();
singleLinkedList.addNodeTail(new ListNode<>(11, null));
singleLinkedList.printLinkedList();
}
@Test
public void test_insertNodeBeforeHead() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.insertNodeBeforeHead(new ListNode<>(10, null), 0);
singleLinkedList.printLinkedList();
singleLinkedList.insertNodeBeforeHead(new ListNode<>(10, null), 1);
singleLinkedList.printLinkedList();
singleLinkedList.insertNodeBeforeHead(new ListNode<>(10, null), 100);
singleLinkedList.printLinkedList();
}
@Test
public void test_insertNodeAfterDummyHead() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.insertNodeAfterDummyHead(new ListNode<>(10, null), 0);
singleLinkedList.printLinkedList();
singleLinkedList.insertNodeAfterDummyHead(new ListNode<>(10, null), 1);
singleLinkedList.printLinkedList();
singleLinkedList.insertNodeAfterDummyHead(new ListNode<>(10, null), 100);
singleLinkedList.printLinkedList();
}
@Test
public void test_deleteNode() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.deleteNode(1);
singleLinkedList.printLinkedList();
singleLinkedList.deleteNode(100);
singleLinkedList.printLinkedList();
}
@Test
public void test_searchNode() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.searchNode(1);
singleLinkedList.searchNode(100);
}
@Test
public void test_reverseSingleLinkedList() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.reverseSingleLinkedList();
singleLinkedList.printLinkedList();
}
@Test
public void test_swapPairs() {
SingleLinkedList<Integer> singleLinkedList = init();
singleLinkedList.swapPairs();
singleLinkedList.printLinkedList();
}
}