一,链表的概念及结构

1,链表概念
LinkedList(链表)是Java列表中的重要一类,用于对批量数据处理,他在逻辑上连续,在物理上非连续,相比于ArrayList(泛型数组),链表的优势在于更快的增加和删除元素。
2,链表的分类
链表按照有无头部节点,是否构成循环,双向还是单向三个特征(2*3)分为八类
单向链表:

双向链表:

3,链表的结构
a,节点
不管是何种链表,底层都是由一个个相互关联的节点对象组成。
单向链表的节点:

双向链表的节点

b,链表头
在有头链表中,第一个节点前会加一个节点,其next指向第一个节点对象,而其val并不存储元素,这个头是不可改变的,是一个固定的对象。
但是为什么在无头链表中我们也见到有链表"头"这个概念呢?其实这里的头节点并非真正的头,他只是一个引用,总是指向链表第一个对象,这个头并没有一个真正属于自己的对象。
c,循环链表
顾名思义这个链表构成了一个闭环,链表的最后一个节点的next指向了第一个节点。
d,下面我们用图描绘一下常见的链表
单向不带头非循环链表

单向带头非循环链表

单向不带头循环链表

双向不带头非循环链表

二,链表的模拟实现
以较为常见的单向不可循环链表为例:
首先我们要在自定义链表类下创建一个内部类对象,里面包含了next和var元素,还需要一个头引用 。
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
1,链表初始化
实例化ListNode对象,通过next值串联
public void initList() {
ListNode listNode1 = new ListNode(1);
ListNode listNode2 = new ListNode(3);
ListNode listNode3 = new ListNode(5);
ListNode listNode4 = new ListNode(6);
ListNode listNode5 = new ListNode(8);
ListNode listNode6= new ListNode(9);
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next=listNode5;
listNode5.next=listNode6;
listNode6.next = null;
this.head = listNode1;
}
2,打印链表
通过一个cur引用不断获取下一个节点并打印其val,直到cur为null
public void show() {
ListNode cur = this.head;
if (cur == null) {
System.out.println("空");
return;
}
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println();
}
3,获取链表长度
和打印链表同理
public int size() {
int size = 0;
ListNode cur = this.head;
if (cur == null) return 0;
while (cur != null) {
size++;
cur = cur.next;
}
return size;
}
4,查找是否存有指定元素,返回boolean值
遍历链表,以指定val作为return条件
public boolean contains(int sum) {
boolean det = false;
ListNode cur = this.head;
while (cur != null) {
if (cur.val == sum) {
det = true;
return det;
}
cur = cur.next;
}
return det;
}
5,链表头部插入
将新节点的next指向head,再将head前移
public void addFirst(int sum) {
ListNode node = new ListNode(sum);
node.next = head;
head = node;
}
6,链表尾部插入
遍历到链表尾巴节点,将其next指向新节点
public void addLast(int sum) {
ListNode node = new ListNode(sum);
ListNode cur = this.head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = node;
}
7,指定下标插入元素
如果指定下标再头部或者尾部,直接调用头插法,不是则调用findIndex找到前一个下标进行插入
public void addIndex(int index, int sum) {
if (index < 0 || index > size()) {
throw new indexOutOfException("链表下标越界");
} else if (index == 0) {
addFirst(sum);
} else if (index == size()) {
addLast(sum);
} else {
ListNode cur = findIndex(index);
ListNode node = new ListNode(sum);
node.next = cur.next;
cur.next = node;
}
}
8,获取指定下标节点的前一个下标
public ListNode findIndex(int index) {
ListNode cur = this.head;
int count = 0;
if (index == 0) {
throw new RuntimeException();
}
while (count != index - 1) {
cur = cur.next;
count++;
}
return cur;
}
9,删除指定var节点
找到指定节点的前一个节点,让他指向后一个节点
public void keyRemove(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;
return;
} else {
prev = prev.next;
cur = cur.next;
}
}
if (head.val == key) {
head = head.next;
}
}
10,删除指定val的所有节点
删除一个节点后不return 而是继续遍历寻找
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 = prev.next;
cur = cur.next;
}
}
if (head.val == key) {
head = head.next;
}
}
11,清空链表
为了防止找不到下一个节点,我们需要在将next赋值为null前,找到下一个节点
public void clear() {
ListNode cur = head.next;
//ListNode nextNode=cur.next;
if (cur == null) return;
while (cur != null) {
ListNode nextNode = cur.next;
cur.next = null;
cur = nextNode;
}
head = null;
}
三,链表面试题
1,反转链表
不断获取下一个节点进行头插,注意为了让cur能找到下一个节点,我们需要一个引用指向cur.next
public ListNode reverseList() {
if (head == null || head.next == 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;
}
2,找到链表的中间节点
可以理解为引用fast的移动速度是slow的两倍,当fast到达终点,slow则指向中间节点
public ListNode findMiddleNode() {
//ListNode cur=head;
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
3,删除中间节点
在查找中间节点的过程中记录下slow的前一个节点,以便找到中间节点后删除
public ListNode deleteMiddle() {
ListNode frontNode = head;
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
frontNode = slow;
slow = slow.next;
}
frontNode.next = slow.next;
return head;
}
4,找到倒数第k个节点
引用的移动距离比slow短k-1个节点,当fast到达末端,slow到达倒数第k个节点
public ListNode findToLastNode(int k) {
if (k <= 0 || k > size()) return null;
ListNode fast = head;
ListNode slow = head;
for (int i = 1; i < k; i++) {
fast = fast.next;
if (fast == null) return null;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
5,合并链表
两个链表各取一个元素进行比较,大的节点接入新链表,并走向下一个节点,重复,直到有一个链表被遍历完,非空部分直接接入新链表
public ListNode mergeTwoLists(ListNode hb) {
ListNode ha = this.head;
ListNode headNew = new ListNode(555);
ListNode tmpH = headNew;
while (ha != null && hb != null) {
if (ha.val <= hb.val) {
tmpH.next = ha;
tmpH = tmpH.next;
ha = ha.next;
} else {
tmpH.next = hb;
tmpH = tmpH.next;
hb = hb.next;
}
}
if (ha != null) {
tmpH.next = ha;
}
if (hb != null) {
tmpH.next = hb;
}
return headNew.next;
}
6,指定元素,大小元素分居两侧
引用bs和be指向较小元素节点的左右两as和ae指向较大元素节点的左右两侧,链表遍历完后合并be和as
public ListNode partition(int x) {
ListNode cur = head;
ListNode bs = null;
ListNode be = null;
ListNode as = null;
ListNode ae = null;
if (cur == null) {
return null;
}
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;
}
be.next = as;
if(as!=null){
ae.next = null;
}
return bs == null ? as : bs;
}
7,判断回文链表(即元素轴对称)
先找到中间节点,然后反转后半段链表,从head和slow这两个引用同时遍历链表
,在两者相遇之前如果一直满足head.var==slow.var则为回文链表
public boolean chkPalindrome(){
ListNode fast=head;
ListNode slow=head;
while (fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
ListNode cur=slow.next;
while (cur!=null){
ListNode curN=cur.next;
cur.next=slow;
slow=cur;
cur=curN;
}
while (head!=slow){
if(head.val!=slow.val){
return false;
}
if(head.next==slow) return true;
head=head.next;
slow=slow.next;
}
return true;
}
8,找到两个链表共同节点
"截"掉长链表的前一部分,使两链表长度相等,同时遍历进行比较,直到找到相同节点或者遍历完链表
public ListNode getIntersectionNode(){
int size1=size();
int size2=size2();
int sizeMin=size1<=size2?size1:size2;
int sizeMax=size1<=size2?size2:size1;
ListNode cur1=size1<=size2?headB:head;
ListNode cur2=size1<=size2?head:headB;
while ((sizeMax-sizeMin)>0){
cur1=cur1.next;
sizeMax--;
}
showNode(cur1);
showNode(cur2);
while (cur2.next!=null){
if(cur1==cur2){
break;
}
cur1=cur1.next;
cur2=cur2.next;
}
if(cur2.next!=null){
return cur1;
}
else {
return null;
}
}
9,找到循环链表的循环点
先判断是否为循环链表,使用快慢指针,若指针相遇则为循环链表,进行查找:
如图c点为指针相遇点,设慢指针路程为s,快指针路程为2*s
则s=ab+bc,2*s=ab+bc+cd+bc,可以解得ab=cb,因此只需要从a点和c点同时遍历链表,当两个引用相遇,即为循环点

public ListNode findRingBegin(){
ListNode cur1=head;
ListNode cur2=head;
if(head==null) return null;
while (cur1.next!=null&&cur1!=null){
cur1=cur1.next.next;
cur2=cur2.next;
if(cur1==cur2) {
ListNode cur=head;
ListNode meet=cur1;
while (cur!=meet){
cur=cur.next;
meet=meet.next;
}
return cur;
}
}
return null;
}
&spm=1001.2101.3001.5002&articleId=143089353&d=1&t=3&u=69624129fee6407a894159b74e51b1b3)
1250

被折叠的 条评论
为什么被折叠?



