本文主要介绍在Java部分中与链表相关的知识。
目录
前言
本文主要介绍在Java的背景下,与链表相关的实现和应用。在阅读本文之前,建议读者优先阅读Java专栏内的文章。
一、链表的概念和结构
我们在前面的文章提到了针对顺序表中间插入和头部插入时效率过于低下、增容时降低运行效率和增容造成空间浪费这些问题,我们可以使用链表来解决这些问题,那么什么是链表呢?实际上,链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。它也是线性表的一种,在物理结构上它不是线性的,但在逻辑结构上一定是线性的。


链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。车厢是独立存在的,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?最简单的做法:每节车厢里都放一把下一节车厢的钥匙。而在链表里,每节 "车厢" 是什么样的呢?

与顺序表不同的是,链表里的每节 "车厢" 都是独立申请下来的空间,我们称之为"结点/节点"节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址。图中变量plist保存的是第一个节点的地址,我们称plist此时 "指向" 第一个节点,如果我们希望plist"指向" 第二个节点时,只需要修改plist保存的内容为0x0012FFA0。但是为什么还需要变量来保存下一个节点的位置?这是因为链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过变量来保存下一个节点位置才能从当前节点找到下一个节点。当我们想要保存一个整型数据时,实际是向操作系统 申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。在我们实际的情况之中,链表有着很多的种类:单向和双向链表,带头指针和不带头指针的链表,循环和非循环链表。



在种类繁多的链表之中,我们需要主要掌握的只有两种,一种是无头结点的单向非循环链表,这是结构最简单的链表,一般来说不会单独用于存放数据,更多的是作为其他数据结构的子结构,比如说哈希桶、图的邻接表等等。

第二种则是无头双向链表,在Java的集合框架库中LinkedList底层就是无头双向循环链表。
二、链表的实现
我们可以借助内部类来自行实现相关的结构:
java
public class MySingleList {
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
}
并且我们把这些需要实现的方法都放到一个接口之中:
java
public interface IList {
//头插法
public void addFirst(int data);
//尾插法
public void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key);
//删除第一次出现关键字为key的节点
public void remove(int key);
//删除所有值为key的节点
public void removeAllKey(int key);
//得到单链表的长度
public int size();
public void clear();
public void display();
}
那这样的话,对于我们前面的相关结构代码我们也要做如下的更新:
java
public abstract class MySingleList implements IList{
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
@Override
public void addFirst(int data) {
}
@Override
public void addLast(int data) {
}
@Override
public void addIndex(int index, int data) {
}
@Override
public boolean contains(int key) {
return false;
}
@Override
public void remove(int key) {
}
@Override
public void removeAllKey(int key) {
}
@Override
public int size() {
return 0;
}
@Override
public void clear() {
}
@Override
public void display() {
}
}
这里我们首先先来看diplay方法,也就是打印链表元素,在这里我们会遇到一个和之前不太一样的问题,就是我们该如何实现让链表自己走下去。换而言之,如果是C语言的话,我们可以通过指针变量来实现遍历元素,但是对于Java来说我们缺乏这种所谓的指针,我们又该如何解决这个难题?
很容易想到的,就是我们仿照C语言那里的思路,去通过head = head.next来实现,也就是下面这个代码:
java
public void display() {
while(head != null){
System.out.print(head.val+" ");
head = head.next;
}
}
这个代码毋庸置疑,是可以实现需求的,但是它在目前我们自行实现的结构,即无头单链表中是有一个致命的缺陷的,那就是我们只要改变原有的head,就必然会导致我们丢失原先定义的head的位置。当然,想解决这个问题还是很简单的,只需略作如下的修改:
java
public void display() {
ListNode cur = head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
}
其他的操作都是比较简单,读者可先自行实现,我这里给出我的实现代码:
java
public abstract class MySingleList implements IList{
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
@Override
public void addFirst(int data) {
ListNode newNode = new ListNode(data);
newNode.next = head;
head = newNode;
}
@Override
public void addLast(int data) {
ListNode newNode = new ListNode(data);
if(head == null){
head = newNode;
return;
}
ListNode cur = head;
while(cur.next != null){
cur = cur.next;
}
cur.next = newNode;
}
@Override
public void addIndex(int index, int data) {
int len = size();
if(index < 0 || index > len){
System.out.println("index位置不合法");
return;
}
if(index == 0){
addFirst(data);
return;
}
if(index == len){
addLast(data);
return;
}
ListNode cur = head;
while((index - 1) != 0){
cur = cur.next;
index--;
}
ListNode newNode = new ListNode(data);
newNode.next = cur.next;
cur.next = newNode;
}
@Override
public boolean contains(int key) {
ListNode cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
@Override
public void remove(int key) {
if(head == null){
return;
}
if(head.val == key){
head = head.next;
}
ListNode cur = findNodeOfKey(key);
if(cur == null){
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
private ListNode findNodeOfKey(int key){
ListNode cur = head;
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
@Override
public void removeAllKey(int key) {
if(head == null){
return;
}
ListNode cur = head.next;
ListNode prev = head;
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}else{
prev = cur;
cur = cur.next;
}
}
if(head.val == key){
head = head.next;
}
}
@Override
public int size() {
ListNode cur = head;
int len = 0;
while(cur != null){
len++;
cur = cur.next;
}
return len;
}
@Override
public void clear() {
ListNode cur = head;
while(cur != null){
ListNode curN = cur.next;
cur.next = null;
cur = curN;
}
head = null;
}
@Override
public void display() {
ListNode cur = head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
}
}
三、链表OJ题
第一道题目测试链接如下:203. 移除链表元素 - 力扣(LeetCode)
这道题正好可以从我们上面的removeAllKey方法进行改造,故而不再赘述:
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head == null){
return head;
}
ListNode cur = head.next;
ListNode prev = head;
while(cur != null){
if(cur.val == val){
prev.next = cur.next;
cur = cur.next;
}else{
prev = cur;
cur = cur.next;
}
}
if(head.val == val){
head = head.next;
}
return head;
}
}

第二道题目测试链接如下:206. 反转链表 - 力扣(LeetCode)
我们这道题采用遍历头插的方法去实现,也就是遍历这些节点,不断头插,具体代码实现如下:
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null){
return head;
}
ListNode cur = head.next;
head.next = null;
while(cur != null){
ListNode curN = cur.next;
cur.next = head;
head = cur;
cur = curN;
}
return head;
}
}

第三道题目测试链接如下:876. 链表的中间结点 - 力扣(LeetCode)
这道题就是很经典的快慢指针来解决,也就是定义两个指针,快指针一次两步,慢指针一次一步,当快指针不再动的时候,返回慢指针的节点即可。
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}

第四道题目测试链接如下:面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)
这道题其实还是可以用快慢指针来解决,只不过这回是通过快指针先走的方法,请读者思考如何解决。
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public int kthToLast(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while((k--) != 0){
fast = fast.next;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
return slow.val;
}
}

第五题测试链接如下:21. 合并两个有序链表 - 力扣(LeetCode)
这道题相对来说是很好解决的,我们可以创建一个新表头,然后遍历两个表的节点来比大小,依次把比较小的部分插入到新表头的后面,然后完成操作。
java
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode headA, ListNode headB) {
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
while(headA != null && headB != null){
if(headA.val < headB.val){
tmp.next = headA;
headA = headA.next;
tmp = tmp.next;
}else{
tmp.next = headB;
headB = headB.next;
tmp = tmp.next;
}
}
if(headA != null){
tmp.next = headA;
}
if(headB != null){
tmp.next = headB;
}
return newHead.next;
}
}

第六题测试链接如下:链表分割_牛客题霸_牛客网
这道题的思路很简单,就是把大于和小于x的数据分到两个不同的链表之中,然后再合并这两个链表即可。
java
import java.util.*;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Partition {
public ListNode partition(ListNode pHead, int x) {
ListNode bs = null;
ListNode be = null;
ListNode as = null;
ListNode ae = null;
ListNode cur = pHead;
while(cur != null){
if(cur.val < x){
if(bs == null){
bs = be = cur;
}else{
be.next = cur;
be = be.next;
}
}else{
if(as == null){
as = ae = cur;
}else{
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
if(bs == null){
return as;
}
be.next = as;
if(as != null){
ae.next = null;
}
return bs;
}
}

总结
本文介绍了Java中链表的基本概念、实现方法和常见OJ题解法。主要内容包括:链表的概念,一种非连续存储结构,通过指针实现逻辑顺序,分为单向/双向、带头/不带头等类型;Java链表实现,使用内部类ListNode,实现插入、删除、查找等基本操作,重点解决了遍历时的指针处理问题;链表OJ题解法,包括移除元素、反转链表、中间节点、倒数第k个节点、合并有序链表和链表分割等典型问题的解决思路和代码实现。文章通过具体代码示例,展示了链表在Java中的实际应用,帮助读者掌握链表这一重要数据结构。