1. ArrayList的缺陷
顺序表只适合静态的查找和更新,不适合插入和删除元素,
因为在ArrayList中插入和删除元素时,由于需要将后序元素往前后者往后移动,所以时间复杂度会相当高,能达到O(N)。
为了解决这一问题,java 引入了 LinkedList(链表)。
2. 链表
2.1 链表的概念以及结构
链表是一种逻辑上连续,物理上不连续的存储结构。
链表是由一个个节点连接构成的,一个节点包含 val 域和 next 域。
逻辑上连续是因为链表有一个 next 域,这个 next 域会指向下一个节点。
每个节点都是一个对象,因此他们都会有属于自己的地址。
上图就是一个不带头单向非循环的链表。
其实,链表的结构有很多种。
1. 带头和不带头
此处的带头和不带头指的是,是否有一个虚拟的头节点,该头节点的 val 是无效值,该头节点的下一个节点才是链表的第一个节点,head 会固定指向该虚拟的头节点,不能修改 head 的指向。
2. 单向和双向
单向和双向,顾名思义,就是只能指向一个方向的和能够指向两个方向的
单向的链表只有两个域(val + next),而双向的链表则比单向的链表多一个域 prev
单向的链表只能向后(next)走,而双向的链表既能向后(next)走,也能向前(prev)走
3. 循环和非循环
顾名思义,就是循环的链表和不是循环的链表咯
虽然链表的种类有这么多,但是我们只用重点掌握两种:单向不带头非循环和双向不带头非循环。
单向不带头非循环:结构简单,面试题考的多。
双向不带头非循环:LinkedList 的底层实现结构就是双向不带头非循环链表。
2.2 链表的实现
此处链表的实现,实现的是单向不带头非循环链表。
要实现一个链表,我们就得先创建一个 MySingleList 类,我们要实现的就是 MySingleList 类。
由刚刚学过的知识可以知道,链表是由一个个节点组成的,节点是链表的一部分
所以我们还需要定义一个静态内部类 ListNode 作为节点。
一个节点 = val + next ,所以 ListNode 有两个成员变量,
我们顺便再把 ListNode 的构造方法写出来。
而链表本身自带一个 head 节点指向头节点,所以我们还需要定义一个成员变量 head
因为没有一个完整的链表,所以我们可以写一个方法,手动创建一个链表,这样方便我们测试这个链表方法写对没。
要实现链表,则需要实现以下方法
//头插法
public void addFirst(int data) {
}
//尾插法
public void addLast(int data) {
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
return false;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
}
//删除所有值为key的节点
public void removeAllKey(int key) {
}
//得到单链表的长度
public int size() {
return -1;
}
//清空链表
public void clear() {
}
//打印链表
public void display() {
}
我们可以先挑选简单的实现,可以选择先实现 size,display,contains 和 clear
那我们就先来实现 size 方法,求链表的长度。
思路就是遍历链表,遇到一个节点,count就++。
那我们就可以定义一个 cur ,cur 指向 head,然后遍历即可
不能直接用 head 来遍历链表,因为如果 head 走了,链表就不知道从哪里开始了。
然后我们再来实现 display ,打印链表
同理,我们要把链表的每一个数字都打印出来,就需要遍历链表,
遍历到一个节点,就打印一个节点。
再来实现 contains 方法,查找 key 是否在链表中存在,
这个也一样,遍历链表,找到了就返回 true , 遍历完链表了还找不到,那就返回 false
再来实现 clear,这不简单,只要将头节点置 null 了,链表也就清空了
再来实现 addFirst ,头插法
头插法的意思就是,新增一个节点,将该节点插入到链表的第一个,也就是头节点的位置。
分两种情况,第一种,链表为空,第二种,链表不为空。
如果链表为空,说明链表里面没有节点,那新增的这个 node 节点就是链表的第一个节点,也就是头节点。
如果链表不为空,那我们需要牢记这一点:插入节点的时候,先绑定后面的数据,再来插入节点
如上图解
再来实现 addLast,尾插法,与头插法类似
尾插法,就是把新增的节点插入到链表的尾巴,也就是链表最后一个节点的位置 。
同理,尾插法也分为两种情况,
第一种,链表为空,说明链表里面一个节点都没有,所以新增的节点,就相当于链表的第一个节点,也就是头节点。
第二种,链表不为空,新增的节点要插入到链表的尾巴,则需要先找到链表的尾巴节点,再来插入。
由上图,我们不难发现,链表尾巴节点和其他节点的区别:那就是尾巴节点的 next 域 为 null
所以,我们就能借助这点,找到链表的尾巴节点。
再来写 addIndex,指定位置插入
因为 addIndex 方法含有参数 index,所以根据 index ,分为四种情况
第一种,非法情况,如果下标小于0或者大于链表的长度,则不合法,不合法就不用插了。
第二种,下标刚好是0,说明是头插法,直接调用我们刚刚写好的 addFirst 即可。
第三种,下标刚好是链表长度,说明是插在最后一个位置,尾插法,直接调用 addLast 即可。
第四种,index 不是以上三种情况,说明 index 位置是在链表中间位置。
则需要我们先找到下标位置的前一个节点,然后绑定数据,再来插入
然后我们再来实现 remove 方法,删除第一个关键字为 key 的节点
那很简单,首先要找到关键字为 key 的节点,再来删除。
如果链表为 null ,说明没有节点给你删除
如果 head.val == key,说明删除头节点
因为要删除 key 的节点,所以得先找到 关键字为 key 的节点的前一个节点,才能进行删除
所以我们可以单独写一个方法,来找,关键字为 key 的节点的前一个节点。
遍历链表,找 key 的节点 的前一个,找到了就返回该节点,找不到就返回 null
找到了 prev(key节点的前一个节点)之后,就直接删除 prev.next 即可
再来实现 removeAllKey 方法,删除所有值为 key 的节点
public class MySingleList {
static class ListNode {
//一个节点包含 val 和 next
public int val;//节点的值域
public ListNode next;//下一个节点的地址
//提供构造方法
public ListNode(int val) {
this.val = val;
}
}
//对于链表本身来说,必须得有一个头结点
public ListNode head;//指向当前链表的头节点
public void createList() {
//创建链表,让每个节点连起来即可
ListNode n1 = new ListNode(12);
ListNode n2 = new ListNode(23);
ListNode n3 = new ListNode(34);
ListNode n4 = new ListNode(45);
ListNode n5 = new ListNode(56);
//通过 next 建立节点之间的联系
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n5;
this.head = n1;
}
//得到单链表的长度
public int size() {
int count = 0;
//用 cur 来遍历链表
ListNode cur = head;
//cur == null说明链表遍历完了
while (cur != null) {
count++;
cur = cur.next;//让 cur 指向下一个节点
}
return count;
}
//打印链表
public void display() {
ListNode cur = head;
//遍历链表
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
//遍历链表,找到了就返回 true
if (cur.val == key) {
return true;
}
cur = cur.next;
}
//找不到就返回 false
return false;
}
//清空链表
public void clear() {
this.head = null;//头节点为null,就找不到其他结点了
}
//头插法
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (head == null) {
head = node;
return;
}
//绑定后面的数据
node.next = head;
//更新头
head = node;
}
//尾插法
public void addLast(int data) {
ListNode node = new ListNode(data);
//如果 head 为 null ,说明链表没有结点
if (head == null) {
head = node;
return;
}
ListNode cur = head;
//找到链表的尾巴节点
while (cur.next != null) {
cur = cur.next;
}
//到了这里说明 cur 指向了尾巴节点
//插入
cur.next = node;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
// index 不合法,则不能插入
if (index < 0 || index > size()) {
return;
}
if (index == 0) {
//头插法
addFirst(data);
return;
}
if (index == size()) {
//尾插法
addLast(data);
return;
}
//中间位置
ListNode cur = head;
//走 index - 1 步,找到要删除结点位置的前一个结点
int step = index - 1;
while (step != 0) {
cur = cur.next;
step--;
}
//先绑定后面的数据
ListNode node = new ListNode(data);
node.next = cur.next;
//再来插入
cur.next = node;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
//空表删不了
if (head == null) {
return;
}
//如果是头节点
if (head.val == key) {
head = head.next;
return;
}
//找到 key 节点的前一个节点
ListNode prev = findKeySubOne(key);
if (prev == null) {
//说明不存在 key 节点的前一个节点
return;
} else {
ListNode target = prev.next;// target 为要删除的节点
//删除
prev.next = target.next;
}
}
private ListNode findKeySubOne(int key) {
//空表
if (head == null) {
return null;
}
ListNode cur = head;
//找到要删除的前一个结点
//必须得判断 cur.next 是否为空,不然有可能会空指针异常
//如果 cur.next == null,那么 cur 已经是尾巴结点,就没有继续判断的必要了
while (cur.next != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
//删除所有值为key的节点
public void removeAllKey(int key) {
//空表
if (head == null) {
return;
}
ListNode prev = head;// prev 为 del 的前一个
ListNode del = head.next;// del 表示要删除的元素
// del 不为空,说明还有,有可能要删除的节点
while (del != null) {
if (del.val == key) {
//删除
prev.next = del.next;
//del往后走
del = del.next;
} else {
//两个都往后走
prev = del;
del = del.next;
}
}
//最后再来判断头节点
if (head.val == key) {
head = head.next;
}
}
}
3. 链表练习题
203. 移除链表元素
刚刚写过删除所有值为 key 的节点,直接复制代码到 LeetCode 上,改下方法名字即可。
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
ListNode prev = head;
ListNode del = head.next;
while (del != null) {
if (del.val == val) {
prev.next = del.next;
del = del.next;
} else {
prev = del;
del = del.next;
}
}
if (head.val == val) {
head = head.next;
}
return head;
}
}
206. 反转链表
这道题,思路就是头插法,从第二个节点开始遍历链表,对链表的每个元素进行头插法即可。
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}
ListNode cur = head.next;//从第二个节点开始反转
ListNode prev = head;// prev 相当于新头
head.next = null;//置空,防止循环
while (cur != null) {
ListNode curNext = cur.next;
//头插法
cur.next = prev;//头插
prev = cur;//更新
cur = curNext;//cur继续往后走
}
return prev;
}
}
876. 链表的中间结点
思路:快慢指针,相同时间,速度两倍,路程两倍
class Solution {
//找中间结点
public ListNode middleNode(ListNode head) {
if (head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
//遍历链表,防止空指针异常
while (fast != null && fast.next != null) {
fast = fast.next.next;//一次走两步
slow = slow.next;//一次走一步
}
return slow;
}
}
链表中倒数第k个结点
思路:还是利用快慢指针,先让 fast 走 k-1步,再让 fast 和 slow 一起走,等 fast 走到尾巴节点, slow 就是倒数第 k 个节点
public class Solution {
public ListNode FindKthToTail(ListNode head, int k) {
// k 不合法或者链表为空
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while (k - 1 != 0) {
fast = fast.next;
if (fast == null) {
//说明k比链表长度还大
return null;
}
k--;
}
//你先走,然后我们同时同速走,距离不变,就是k-1步,差k个节点
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
21. 合并两个有序链表
思路:与合并2个有序数组类似,定义一个dummy虚拟头节点,如果链表1的节点比链表2的节点小,那就链表1的节点加进来,如果不是,那就链表2的节点加进来,然后对应的链表往后走,遍历完之后,看看哪个链表不为空,不为空的链表就连接在后面,最后返回 dummy.next 即可。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null && list2 == null) {
return null;
}
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
if (list1 != null) {
cur.next = list1;
}
if (list2 != null) {
cur.next = list2;
}
return dummy.next;
}
}
CM11 链表分割
思路:与合并有序链表类似,但又不完全类似,定义bs,be,as,ae,两个部分
小于x放到 b 中,大于的放到 a 中,最后让 be 和 as 连接即可
public class Partition {
public ListNode partition(ListNode pHead, int x) {
//定义 bs,be,as,ae
// 小于 x 放到 b ,大于 x 放到 a 中
//最后连接 be 和 as
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 = cur;
be = cur;
} else {
be.next = cur;
be = cur;
}
} else {
//第一次
if (as == null) {
as = cur;
ae = cur;
} else {
ae.next = cur;
ae = cur;
}
}
cur = cur.next;
}
//如果第一段为空
if (bs == null) {
return as;
}
//如果a段存在,ae.next需要置空,防止形成循环
if (as != null) {
ae.next = null;
}
//连接a段和b段
be.next = as;
return bs;
}
}
OR36 链表的回文结构
思路:1. 找到链表的中间节点 。 2. 反转中间节点之后的节点。 3. 比较
public class PalindromeList {
public boolean chkPalindrome(ListNode head) {
if (head == null) {
return true;
}
ListNode fast = head;
ListNode slow = head;
//1. 找到中间节点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//2. 反转中间节点之后的节点
ListNode cur = slow.next;
while (cur != null) {
ListNode curNext = cur.next;//记录cur的下一个节点
cur.next = slow;//反转
slow = cur;//更新头
cur = curNext;//cur往后走
}
//3. 比较
while (head != slow) {
//值不相同
if (head.val != slow.val) {
return false;
}
节点个数为偶数个时,相遇的前奏就是这个
if (head.next == slow) {
return true;
}
head = head.next;
slow = slow.next;
}
return true;
}
}
160. 相交链表
思路:
1.相交主要是Y字型
2.两个链表的长度不一样 主要体现在相交之前
3.可以让最长的链表先走它们的差值步,然后两个链表再一起走
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null && headB == null) {
return null;
}
//1.分别求出两个链表的长度,然后做差
ListNode pL = headA;//假设A是较长的
ListNode pS = headB;
int lenA = 0;
int lenB = 0;
while (pL != null) {
lenA++;
pL = pL.next;
}
while (pS != null) {
lenB++;
pS = pS.next;
}
int len = lenA - lenB;//记录差值
pL = headA;//重新回到头结点
pS = headB;
//进行修正
if (len < 0) {
pL = headB;
pS = headA;
len = lenB - lenA;
}
//2.让较长的链表先走差值步
while (len != 0) {
pL = pL.next;
len--;
}
//3.然后两个链表再一起走
while (pL != null && pS != null) {
if (pL == pS) {
return pL;
}
pL = pL.next;
pS = pS.next;
}
return null;
}
}
141. 环形链表
思路:利用快慢指针,fast 一次走2步,slow 一次走一步,如果有环,那么能追上,没环就肯定追不上
public class Solution {
//是否有环
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;//一次走两步
slow = slow.next;//一次走一步
//相当于追及问题,slow在追fast,有环就能追上,没环就追不上
if (fast == slow) {
return true;
}
}
return false;
}
}
142. 环形链表 II
思路:快慢指针,当 fast 与 slow 相遇时,让 fast 从 head 开始,和 slow 同速一起走,当 slow
和 fast 再次相遇时,该相遇点就是环的入口点
public class Solution {
//返回环的入口点
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
//相遇后,
//相遇点到入口点的距离与起始点到入口点的距离相等
fast = head;
while (fast != null && slow != null) {
if (fast == slow) {
return fast;//如果fast与slow再次相遇,则该点为入口点
}
fast = fast.next;
slow = slow.next;
}
}
}
return null;
}
}
4. LinkedList的模拟实现
和单链表同理,只是双向无头非循环链表多了一个 prev 域
与单链表相同,双向链表也由一个个节点组成,所以同理,我们要定义一个静态内部类,相比于单链表,双链表还多了一个尾巴节点,尾巴节点 last 会指向双链表的最后一个节点
双向链表与单链表相同,也要实现以下方法
//头插法
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 display() {
}
public void clear() {
}
同样的,我们先选择实现 size,display,contains 方法
这些方法的实现与单链表没什么区别,直接按照你写单链表的方式来写上述三个方法即可。
再来实现 clear 方法,需要注意,每一个节点的 prev 和 next 都要手动置空,last 和 head 也要置空
再来实现头插法,与单链表类似,分两种情况
第一种,链表为空,说明新增的节点既是头节点又是尾巴节点
第二种,链表不为空,则先需要绑定后面的数据,再来插入
再来实现尾插法,同样与单链表类似,分两种情况
第一种,链表为空,相当于头插
第二种,链表不为空,先绑定数据,再来插入
再来实现 addIndex,与单链表类似,分四种情况
第一种,index 不合法,不能插入
第二种,index = 0,相当于头插法
第三种,index = size() ,相当于尾插法
第四种,不是以上三种情况,首先先找到 index 下标的节点,先绑定数据,再进行插入
第四种情况与单链表类似,先让 cur 走 index 步,然后绑定数据,进行插入。
然后我们再来实现 remove 方法,
思路:遍历链表,如果要删除的是头节点,就需要考虑该链表是否只有一个节点,
如果要删除的不是头节点,那就得判断要删除的节点是尾巴节点,还是中间节点
再来实现 removeAllKey 方法,思路与 remove 方法类似,只不过 remove 方法是删一次就走人了,而 removeAllKey 方法,是要删除多次
// 无头双向链表实现
public class MyLinkedList {
static class ListNode {
public int val;//节点的值域
public ListNode prev;//指向上一个节点
public ListNode next;//指向下一个节点
//提供构造方法
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//头节点
public ListNode last;//尾巴节点
//得到链表的长度
public int size() {
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
}
//查找是否包含关键字key是否在链表当中
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
public void clear() {
ListNode cur = head;
//要将链表的每一个元素手动置空
while (cur != null) {
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
cur = curNext;
}
this.head = null;
this.last = null;
}
//头插法
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (head == null) {
head = node;
last = node;
} else {
//先绑定数据
node.next = head;
head.prev = node;
//再来插入
head = node;///更新头
}
}
//尾插法
public void addLast(int data) {
if (head == null) {
addFirst(data);
} else {
ListNode node = new ListNode(data);
//先绑定数据
node.prev = last;
last.next = node;
//再进行插入
last = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index, int data) {
if (index < 0 || index > size()) {
return;
}
if (index == 0) {
addFirst(data);
return;
}
if (index == size()) {
addLast(data);
return;
}
ListNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
ListNode node = new ListNode(data);
ListNode prev = cur.prev;
//先绑定数据
node.prev = prev;
node.next = cur;
//再来插入
prev.next = node;
cur.prev = node;
}
//删除第一次出现关键字为key的节点
public void remove(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//删除头节点
if (cur == head) {
head = head.next;
//考虑只有一个节点的情况
if (head == null) {
last = null;
} else {
head.prev = null;
}
} else {
//是否是尾巴节点
if (cur.next == null) {
//删除尾巴节点
last = last.prev;
last.next = null;
} else {
//删除中间节点
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
//删完之后就走人
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
//删除头节点
if (cur == head) {
head = head.next;
//考虑只有一个节点的情况
if (head == null) {
last = null;
} else {
head.prev = null;
}
} else {
//是否是尾巴节点
if (cur.next == null) {
//删除尾巴节点
last = last.prev;
last.next = null;
} else {
//删除中间节点
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
cur = cur.next;//继续删
} else {
cur = cur.next;
}
}
}
}
5. LinkedList的使用
5.1 LinkedList的构造
5.2 LinkedList的其他常用方法介绍
刷题的时候直接用就行
5.3 LinkedList的遍历
可以使用 foreach 循环遍历,或者使用迭代器遍历文章来源:https://www.toymoban.com/news/detail-696437.html
public static void main(String[] args) {
List<Integer> list = new LinkedList<>();
list.add(1);
// 1.使用 foreach 遍历
for (int x : list) {
System.out.print(x + " ");
}
// 2.使用迭代器遍历
Iterator<Integer> iterator = list.listIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
}
6. ArrayList和LinkedList的区别
文章来源地址https://www.toymoban.com/news/detail-696437.html
到了这里,关于数据结构之链表 - 超详细的教程,手把手教你认识并运用链表的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!