接上次博客:和数组处理有关的一些OJ题;ArrayList 实现简单的洗牌算法(JAVA)(ArrayList)_di-Dora的博客-CSDN博客
目录
链表的基本概念
链表的类型
单向、不带头、非循环链表的实现
遍历链表并打印节点值:
在链表头部插入节点:
在链表尾部插入节点:
得到单链表的长度 :
查找是否包含关键字key是否在单链表当中:
删除第一次出现关键字为key的节点 (两种实现方式):
删除所有值为key的节点:
指定任意位置插入数据:
清空链表:
OJ练习
链表的优缺点
数组是一块连续的内存,逻辑上和物理内存上都是连续的;
链表是在逻辑上是连续的,但是在物理内存上是不连续的。
链表的基本概念
链表是一种常见的数据结构,它由一系列节点组成,每个节点包含两部分:数据元素 (value) 和指向下一个节点的指针 ( next 域 )。通过这些节点的连接,可以形成一个链式结构。
链表的基本概念如下:
1、节点(Node):链表的基本单元,包含数据元素和指针。数据元素可以是任意类型的数据,指针指向下一个节点。每个节点都是一个对象。最后一个节点的 next 域是 null 。
2、头节点(Head):链表的第一个节点,用于标识链表的起始位置。通常使用一个指针变量来指向头节点。
3、尾节点(Tail):链表的最后一个节点,其指针指向空(NULL),表示链表的结束。
4、链表长度(Length):链表中节点的数量,可以通过遍历链表来计算。
5、空链表(Empty List):不包含任何节点的链表。
6、单向链表(Singly Linked List):每个节点只有一个指针,指向下一个节点。最后一个节点的指针指向空。
7、双向链表(Doubly Linked List):每个节点有两个指针,一个指向前一个节点,一个指向下一个节点。头节点的前一个指针和尾节点的后一个指针都指向空。
注意:
1.链式结构在逻辑上是连续的,但是在物理上不一定连续;
2.现实中的节点一般都是从堆上申请出来的;
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
链表的类型
链表的组合方式有多种,可以根据以下两个方面来区分和计算组合的种类:
1、单向链表和双向链表:
根据节点的指针数量,链表可以分为单向链表和双向链表。
单向链表每个节点只有一个指针,指向下一个节点;
而双向链表每个节点有两个指针,分别指向前一个节点和后一个节点。
2、是否带头节点:
带头节点的链表在第一个节点之前有一个额外的头节点,用于标识链表的起始位置。(head的value是无意义的,如果想从最开头插入数据时,head是不可变的,从head后面插入)
而不带头节点的链表则直接以第一个节点作为链表的起始位置。(head是有value的,如果想从最开头插入数据时,head是可变的,变成新插入的数据)
3、是否循环:
循环链表是在链表的尾部节点和头部节点之间形成一个循环连接,使得链表的最后一个节点指向头部节点。
综合考虑上述两个方面,我们可以得到链表的组合方式共有8种:
单向、不带头节点、非循环链表(重点)
单向、不带头节点、循环链表
单向、带头节点、非循环链表
单向、带头节点、循环链表
双向、不带头节点、非循环链表(重点)
双向、不带头节点、循环链表
双向、带头节点、非循环链表
双向、带头节点、循环链表
每种组合方式都有自己的特点和应用场景,我们可以根据具体需求选择合适的链表类型。
单向、不带头、非循环链表的实现
我们可以先来实现一个最简易的链表,即手动创建一个单向链表:
public class MySingleList {
static class ListNode {
public int val; // 节点的值域
public ListNode next; // 下一个节点的地址
public ListNode(int val) {
this.val = val;
}
}
public ListNode head; // 表示当前链表的头节点
//我们先来写一个最笨的方法:手动创建链表节点
public void createlist() {
// 创建链表节点
head = new MySingleList.ListNode(-1);
MySingleList.ListNode node1 = new MySingleList.ListNode(12);
MySingleList.ListNode node2 = new MySingleList.ListNode(23);
MySingleList.ListNode node3 = new MySingleList.ListNode(34);
MySingleList.ListNode node4 = new MySingleList.ListNode(45);
MySingleList.ListNode node5 = new MySingleList.ListNode(56);
// 构建链表关系
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
this.head = node1;//head 是一个指向第一个节点的引用
}
}
public class Test {
public static void main(String[] args) {
MySingleList list = new MySingleList();
list.createlist();
System.out.println(list);
System.out.println("12345");
}
}
通过这个代码,我们可以直观地观察到链表的大致结构:
好了,现在我们就正式开始实现一个完整的单向链表了:
首先我们还是先给出链表的基本代码:
我们先要有一个引用 head 指向第一个节点,它是“节点”类型,就如同 Person person = new Person; 一样。
链表的头节点,是链表的成员变量、链表的属性,而不是一个节点类的成员变量。
public class MySingleList {
static class ListNode {
public int val; // 节点的值域
public ListNode next; // 下一个节点的地址
public ListNode(int val) {
this.val = val;
}
}
public ListNode head; // 表示当前链表的头节点
// 在链表头部插入节点
public void insertAtHead(int val)
// 在链表尾部插入节点
public void insertAtTail(int val)
//得到单链表的长度
public int size()
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key)
//删除第一次出现关键字为key的节点
public void deleteNode(int key)
// 删除所有值为key的节点
public void removeAllKey(int key)
//任意位置插入,第一个数据节点为0号下标
public void insertAtIndex(int index, int val)
// 遍历链表并打印节点值
public void display()
// 清空链表
public void clear()
}
以上都是我们需要实现的方法。
先来实现第一个:
遍历链表并打印节点值:
// 遍历链表并打印节点值
public void display() {
//不可以让head本身移动,否则将遗失head的位置
ListNode curr = head;
while (curr != null) {
System.out.print(curr.val + " ");
curr = curr.next; //引用向后移动一位
}
System.out.println();
}
这里我们要注意:curr 是一个引用!!!
curr = null 代表的是已经遍历了整个链表。
在链表头部插入节点:
// 在链表头部插入节点
//一般建议,再插入的时候,先绑定后面的节点信息
//就算链表中一个代码都没有,也不影响我们插入节点
//以头插法插入,数据是倒序的
public void insertAtHead(int val) {
ListNode newNode = new ListNode(val);
newNode.next = head;
head = newNode;
}
public static void main(String[] args) {
MySingleList list = new MySingleList();
//list.createlist();
list.insertAtHead(12);
list.insertAtHead(23);
list.insertAtHead(34);
list.insertAtHead(45);
list.insertAtHead(56);
list.display();
}
注意:以头插法插入,数据是倒序的:
在链表尾部插入节点:
// 在链表尾部插入节点
public void insertAtTail(int val) {
ListNode newNode = new ListNode(val);
//cur = null 代表把链表的每一个节点都遍历完了
//cur.next = null 代表cur现在是最后一个节的位置
//一定要写,否则会报:空指针异常
//如果head等于null,curr也就等于null,就不存在curr.next
if (head == null) {
head = newNode;
} else {
ListNode curr = head;
while (curr.next != null) {
curr = curr.next;
}
curr.next = newNode;
}
}
注意区分:
curr = null 表示当前节点 curr 引用已经指向了链表的末尾,即已经遍历完了链表的所有节点。在这种情况下,可以用来判断是否已经遍历到了链表的末尾。
curr.next = null 表示当前节点 curr 的下一个节点指针指向 null,即当前节点 curr 是链表中的最后一个节点。这通常用于在遍历链表时进行判断,以确定是否已经到达了链表的末尾节点。
得到单链表的长度 :
//得到单链表的长度
public int size() {
int length = 0;
ListNode curr = head;
while (curr != null) {
length++;
curr = curr.next;
}
return length;
}
查找是否包含关键字key是否在单链表当中:
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
ListNode curr = head;
while (curr != null) {
if (curr.val == key) {
return true;
}
curr = curr.next;
}
return false;
}
删除第一次出现关键字为key的节点 (两种实现方式):
找到你要删除的节点的前驱,用 del = curr.next;进行删除:curr.next = del.next;
//删除第一次出现关键字为key的节点
//找到指定删除的节点的前一个节点,即找到key的前驱
public void deleteNode(int key) {
if (head == null) {
System.out.println("当前链表无数据");
return;
}
//单独删除头节点
if (head.val == key) {
head = head.next;
return;
}
ListNode curr = head;
//如果 curr.next = null ,表示已经没有下一个节点了
while (curr.next != null) {
if (curr.next.val == key) {
curr.next = curr.next.next;
return;
}
//curr 后移,继续往后寻找
curr = curr.next;
}
}
//删除第一次出现关键字为key的节点 -------第2种方法
public void remove(int key){
if(head == null) {
System.out.println("当前链表无数据");
return;
}
//单独删除头节点
if(head.val == key) {
head = head.next;
return;
}
ListNode cur = searchPrev(key);
if(cur == null) {
System.out.println("没有你要删除的数字");
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
private ListNode searchPrev(int key) {
ListNode cur = head;
while (cur.next != null) {
if(cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
删除所有值为key的节点:
删除所有值为key的节点?那我们遍历链表直到找不到key不就好了?
不可以想得那么简单!我们需要快速的一次性删除!
我们需要定义两个引用:
curr:代表当前需要删除的节点;prev:代表要删除节点的前驱。
如果头节点的 val 就是 key 怎么办?
我们先来看看第一种写法:
public void removeAllKey(int key) {
if(head==null){
return;
}
ListNode prev = head;
ListNode curr = head.next;
while (curr != null) {
if (curr.val == key) {
prev.next = curr.next;
} else {
prev = curr;
}
curr = curr.next;
}
//删除头节点
if(head.val==key){
head=head.next;
}
}
可不可以把我们最后的
//删除头节点
if(head.val==key){
head=head.next;
}
放到前面呢?
如果将删除头节点的代码放到前面,可能会导致以下问题:
如果我们将删除头节点的代码放到循环的前面,那么在进入循环之前,我们会执行删除头节点的操作。这意味着我们将删除链表的头节点,并将指针 head 指向下一个节点。此时,prev 和 curr 指针都指向了同一个节点,即原链表的第二个节点。
然后,循环开始执行,根据通常的逻辑,我们应该检查当前节点 curr 的值是否等于目标值 key,并相应地删除节点。然而,在这种情况下,由于 prev 和 curr 指向相同的节点,将 prev 和 curr 都指向下一个节点,而不检查该节点的值是否等于 key。
这样就会导致我们跳过了一个节点,下一次循环中的 curr 实际上已经指向了原链表中的第三个节点,而不是第二个节点。因此,我们没有对当前节点进行值的检查,可能会导致跳过了一个需要删除的节点。
这种错误的结果是因为删除头节点的操作被放置在了循环之前,导致循环内的删除操作出现了逻辑错误。正确的做法是在循环中进行节点的删除操作,并根据节点的值进行判断和处理,而不是提前删除头节点。
因此,将删除头节点的代码放到前面会导致以上问题。为了确保算法正确地删除所有的值等于 key 的节点,需要将删除头节点的代码放在循环之后,这样我们可以正确地处理链表中的所有节点。
那还有没有别的方法?
我们来看看第二种写法:
// 删除所有值为key的节点
public void removeAllKey(int key) {
ListNode dummy = new ListNode(0); // 创建一个虚拟头节点,方便处理头节点的情况
dummy.next = head;
ListNode prev = dummy;
ListNode curr = head;
while (curr != null) {
if (curr.val == key) {
prev.next = curr.next;
} else {
prev = curr;
}
//prev不可以移动!可能下一个节点仍为key!
curr = curr.next;
}
head = dummy.next;
}
这段代码采用了虚拟头节点的方式来简化对头节点的处理:
首先,代码创建了一个名为 dummy 的虚拟头节点,并将其指向原链表的头节点,即 dummy.next = head。这样做是为了在处理头节点时能够与其他节点一样进行相同的操作。
然后,定义了两个指针 prev 和 curr,初始时 prev 指向虚拟头节点 dummy,curr 指向原链表的头节点 head。
接下来,进入了一个循环,循环条件是 curr 不为 null,即遍历链表直到 curr 为最后一个节点。
在循环内部,首先判断当前节点 curr 的值是否等于目标值 key。如果相等,表示需要删除该节点。此时,将 prev.next 指向 curr.next,即将 prev 的下一个节点指向 curr 的下一个节点,实现了删除当前节点的操作。
如果当前节点的值不等于目标值 key,则将 prev 移动到当前节点 curr 的位置,即 prev = curr。这样做是为了保持 prev 始终指向当前节点的前一个节点,方便在需要删除节点时修改链表的连接关系。
无论当前节点的值是否等于目标值 key,最后都将 curr 指向下一个节点,即 curr = curr.next,继续遍历下一个节点。
循环结束后,原链表中所有值为 key 的节点都已经被删除,此时需要更新头节点的指向。将 head 指向虚拟头节点的下一个节点,即 head = dummy.next,完成了删除操作。
总之,该方法使用虚拟头节点来简化对头节点的处理,通过遍历链表,找到需要删除的节点,并修改节点间的连接关系,最终实现了删除链表中所有值为 key 的节点的功能。
指定任意位置插入数据:
定义一个引用 curr,让它走到即将插入位置的前一个位置,这样我们可以同时访问到插入位置前和插入位置后的节点。先把 curr.next 赋值给newNode.next ,即新插入节点的指向原来位于插入位置的节点,再把 curr.next 变成 newNode 的值。
往 0 位置插入,相当于头插法,往结尾插入,相当于尾插法。
//任意位置插入,第一个数据节点为0号下标
public void insertAtIndex(int index, int val) {
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException("Invalid index: " + index);
}
if (index == 0) {
insertAtHead(val);
return;
}
if(index==size()){
insertAtTail(val);
return;
}
ListNode newNode = new ListNode(val);
ListNode curr = head;
int count = 0;//定义一个计数器
while (curr != null && count < index - 1) {
curr = curr.next;
count++;
}
if (curr == null) {
throw new IndexOutOfBoundsException("Invalid index: " + index);
}
newNode.next = curr.next;
curr.next = newNode;
}
或者,你也可以单独封装出去一个方法:
private ListNode findIndexSubOne(int index){
ListNode curr=head;
while (index-1!=0){
curr=curr.next;
index--;
}
return curr;
}
清空链表:
// 清空链表
public void clear() {
head = null;
}
最后可以测试了看看:
public class Test {
public static void main(String[] args) {
MySingleList list = new MySingleList();
//list.createlist();
list.insertAtHead(12);
list.insertAtHead(23);
list.insertAtHead(34);
list.insertAtHead(45);
list.insertAtHead(56);
list.display();
list.insertAtTail(666);
list.display();
list.deleteNode(12);
list.display();
list.insertAtTail(23);
list.insertAtTail(34);
list.insertAtTail(45);
list.insertAtTail(23);
list.display();
list.removeAllKey(23);
list.display();
list.insertAtIndex(2,99999);
list.display();
list.insertAtIndex(5,188);
list.display();
int lengh=list.size();
System.out.println(lengh);
}
}
现在我们以及了解了链表大致方法的底层逻辑了,为了巩固知识,接下来,我们一起做一些OJ练习吧。
OJ练习
1、给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
这是一个笔试面试里面经常考察的问题,所以蛮重要的。
使用头插法:
(1)、迭代:
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 reverseList1() {
if(head == null) return null;
if(head.next == null) return head:
//cur从第二个节点开始
ListNode cur = head.next;
//先将第一个节点next 置为空,因为它一定是最后一个节点
head.next = null:
while(cur != null) {
//记录下来 当前需要翻转的节点的下一个节点
ListNode curNext = cur.next;
cur.next = head;
head = cur; // 将 cur 设置为新的头节点
cur = curNext;
}
return head;
}
public ListNode reverseList2(ListNode head) {
if(head==null){return null;}
if(head.next==null){return head;}
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextTemp = curr.next; // 暂存当前节点的下一个节点
curr.next = prev; // 当前节点的指针指向前一个节点
prev = curr; // prev 指针向后移动
curr = nextTemp; // curr 指针向后移动
}
return prev; // prev 最终指向反转后的头节点
}
}
第一种方法中,使用了两个指针 cur 和 curNext,以及一个变量 head 来记录头节点。在每次迭代中,将当前节点 cur 的 next 指针指向前一个节点 head,然后更新 head 为 cur,最后将 cur 更新为下一个节点 curNext。最终返回 head 作为反转后的链表头节点。
第二种方法中,使用了两个指针 prev 和 curr,分别表示当前节点的前一个节点和当前节点。
- 首先进行特殊情况的处理。如果链表为空或只有一个节点,直接返回链表本身。
- 初始化两个指针 prev 和 curr,分别指向前一个节点和当前节点,初始时 prev 为null,curr 指向链表的头节点。
- 进入循环,循环条件为 curr 不为null。
- 在循环内部,首先暂存当前节点 curr 的下一个节点,将其保存在 nextTemp 中,以防断开链表。
- 将当前节点 curr 的指针指向前一个节点 prev,实现反转操作。
- 更新 prev 指针为当前节点 curr,将其向后移动。
- 更新 curr 指针为暂存的下一个节点 nextTemp,将其向后移动。
- 循环结束后,链表的所有节点都被反转,并且 prev 指向了反转后的头节点。
- 返回 prev,即为反转后的链表的头节点。
大概如下图:相当于创建了一个节点作为最后的尾巴,反正是无意义的:
(2)、递归:
public ListNode reverseList(ListNode head) {
// 递归终止条件:如果链表为空或只有一个节点,则直接返回该节点
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next); // 递归反转后续链表
head.next.next = head; // 将当前节点的下一个节点的指针指向当前节点,实现反转
head.next = null; // 将当前节点的指针指向 null,避免形成环
return newHead; // 返回反转后的头节点
}
这两种方法都可以实现链表的反转。迭代方法通过维护两个指针 prev 和 curr 来逐个反转节点的指针指向,直至遍历完整个链表。递归方法则通过递归调用先反转后续链表,再修改当前节点和后续节点的指针指向来实现反转。最后,两种方法都返回反转后的头节点。
2、给你单链表的头结点 head,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
要找出链表的中间节点,我们可以使用“快慢指针”的思想:
定义两个指针,一个慢指针 slow 和一个快指针 fast,初始时都指向链表的头节点 head。
快指针 fast 每次移动两步,慢指针 slow 每次移动一步。当快指针到达链表末尾时,慢指针恰好到达链表的中间位置。
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;
}
在每次迭代中,快指针 fast 先向后移动两步,如果链表长度为奇数,则慢指针 slow 恰好指向中间节点;如果链表长度为偶数,则慢指针 slow 指向中间两个节点的后一个节点。
最终,返回慢指针 slow 所指向的节点作为链表的中间节点。
但是,请注意,上述代码假设链表的头节点不为 null,并且没有循环或环形结构。如果链表可能存在环,请先检查是否有环再应用上述算法。
还有,我们这个地方:
while (fast != null && fast.next != null)
不可以写成:
while (fast.next != null && fast != null )
因为,当 fast 为 null 时,如果我们先判断 fast.next != null,会出现 NullPointerException。因为当 fast 为 null 时,无法继续访问 fast.next,会抛出异常。
3、输入一个链表,输出该链表中倒数第k个结点。
要输出链表中倒数第k个节点,我们还是可以使用双指针的方法:
要找到链表中倒数第 k 个节点,可以使用双指针法。定义两个指针,一个指针 fast 和一个指针 slow,初始时都指向链表的头节点 head。
首先,将 fast 指针向前移动 k-1 步,使得 fast 指针和 slow 指针之间相隔 k-1 个节点。然后,同时移动 fast 和 slow 指针,直到 fast 指针到达链表的末尾。此时,slow 指针指向的节点就是倒数第 k 个节点。
如果链表的长度小于 k,即链表节点数不足 k 个,则无法找到倒数第 k 个节点,返回 null。
public ListNode FindKthToTail(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
ListNode slow = head;
// 将 fast 指针向前移动 k-1 步
for (int i = 0; i < k - 1; i++) {
if (fast.next != null) {
fast = fast.next;
} else {
// 如果链表长度小于 k,返回 null
return null;
}
}
// 同时移动 fast 和 slow 指针
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
在代码中,首先进行一些边界条件的判断,如果链表为空或者 k 的值小于等于 0,直接返回 null。
然后,使用快指针 fast 先向前移动 k-1 步。在移动过程中需要注意判断是否已经到达链表末尾,如果到达末尾但还没有移动 k-1 步,则链表长度不足 k,返回 null。
接下来,使用快指针 fast 和慢指针 slow 同时移动,直到 fast 指针到达链表末尾。此时,slow 指针指向的节点就是倒数第 k 个节点。
最后,返回 slow 指针指向的节点作为结果。
需要注意的是,在处理边界情况时要进行额外的判断,例如链表长度小于 k 或链表长度等于 k 的情况。
4、将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
这也是一道经典题型!
(1)、迭代法:
- 创建一个新的虚拟头节点 dummy,以及一个指针 curr 指向 dummy。
- 在每次迭代中,比较两个链表的当前节点 list1 和 list2 的值,将较小值的节点接到 curr 的后面,并将对应链表的指针向后移动一位。
- 最终,当其中一个链表到达末尾时,将另一个链表的剩余部分直接接到 curr 的后面。
- 返回 dummy.next,即为合并后的链表头节点。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0); // 创建虚拟头节点
ListNode curr = dummy; // 当前节点指针
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
curr.next = list1;
list1 = list1.next;
} else {
curr.next = list2;
list2 = list2.next;
}
curr = curr.next;
}
// 将剩余的链表部分直接接到 curr 的后面
if (list1 != null) {
curr.next = list1;
}
if (list2 != null) {
curr.next = list2;
}
return dummy.next; // 返回合并后的链表头节点
}
(2)、递归法:
- 递归地比较两个链表的当前节点 list1 和 list2 的值,选择较小值的节点作为合并后的链表的当前节点,并将其 next 指针指向递归调用的结果。
- 递归终止条件是当其中一个链表为空时,直接返回另一个链表。
- 返回合并后的链表头节点。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null) {
return list2;
}
if (list2 == null) {
return list1;
}
if (list1.val <= list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
5、现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
我们又可以使用两个指针来实现:
(1)、创建两个新的链表,smallerHead 和 greaterHead,分别代表小于 x 的节点和大于等于 x 的节点的链表。同时创建两个尾节点指针 smallerTail 和 greaterTail,初始时它们都指向对应链表的头节点。
(2)、遍历原始链表 pHead:
- 如果当前节点的值小于 x,将其插入到 smallerTail 的后面,并将 smallerTail 指向新插入的节点,更新 smallerTail。
- 如果当前节点的值大于等于 x,将其插入到 greaterTail 的后面,并将 greaterTail 指向新插入的节点,更新 greaterTail。
(3)、遍历完原始链表后,将 smallerHead 的尾节点 smallerTail 连接到 greaterHead 的头节点之后,形成新的链表。
(4)、将 greaterTail 的尾节点的 next 指针设置为 null,确保新链表的尾节点的 next 为 null。
返回新链表的头节点 smallerHead.next,即小于 x 的节点排在前面的链表的头节点。
图
public ListNode partition(ListNode pHead, int x) {
// write code here
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;
}
public class Partition {
public ListNode partition(ListNode pHead, int x) {
ListNode smallerHead = new ListNode(0); // 用于存储小于 x 的节点的链表
ListNode greaterHead = new ListNode(0); // 用于存储大于等于 x 的节点的链表
ListNode smallerTail = smallerHead; // smallerTail 指向 smallerHead 的尾节点
ListNode greaterTail = greaterHead; // greaterTail 指向 greaterHead 的尾节点
while (pHead != null) {
if (pHead.val < x) {
smallerTail.next = pHead;
smallerTail = smallerTail.next;
} else {
greaterTail.next = pHead;
greaterTail = greaterTail.next;
}
pHead = pHead.next;
}
// 将两个链表连接起来
smallerTail.next = greaterHead.next;
greaterTail.next = null; // 确保最后一个节点的 next 为 null
return smallerHead.next; // 返回新链表的头指针
}
}
6、对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
比如:1->2->2->1
图
要判断一个链表是否为回文结构,我们还是可以使用快慢指针和链表反转的方法。
分奇偶讨论:
奇数:
偶数:
(1)、使用快慢指针找到链表的中间节点。
- 初始化快指针 fast 和慢指针 slow,均指向链表的头节点 A。
- 使用循环,每次将快指针 fast 向后移动两步,慢指针 slow 向后移动一步,直到快指针 fast 到达链表尾部或倒数第二个节点。
- 如果链表长度为奇数,快指针 fast 将指向链表的最后一个节点;如果链表长度为偶数,快指针 fast 将指向空节点。
- 此时慢指针 slow 指向链表的中间节点。
(2)、反转链表的后半部分。
- 从慢指针 slow 开始,将链表的后半部分进行反转。
- 使用三个指针 prev、curr、next,进行链表的反转操作。具体步骤如下:
- 初始化 prev 为 null,curr 为 slow,next 为 null。
- 使用循环,将 curr 的下一个节点保存到 next。
- 将 curr 的下一个节点指向 prev,实现链表的反转。
- 将 prev 移动到 curr,将 curr 移动到 next,继续进行下一轮反转操作。
- 当 curr 为 null 时,表示链表的后半部分已经反转完成。
(3)、判断链表是否为回文结构。
- 从头节点 A 和反转后的链表的头节点开始,逐个比较节点的值。
- 如果有任何节点的值不相等,则链表不是回文结构,返回 false。
- 如果所有节点的值都相等,链表是回文结构,返回 true。
public boolean chkPalindrome(ListNode head) { // 1. 找到中间位置 ListNode fast = head; ListNode slow = head; 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.next = slow; slow = cur; cur = curNext; } //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; }
public boolean chkPalindrome(ListNode A) { if (A == null || A.next == null) { return true; // 链表为空或只有一个节点时,视为回文结构 } ListNode fast = A; // 快指针 ListNode slow = A; // 慢指针 // 使用快慢指针找到链表的中间节点 while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } // 反转链表的后半部分 ListNode prev = null; ListNode curr = slow; ListNode next = null; while (curr != null) { next = curr.next; curr.next = prev; prev = curr; curr = next; } // 比较链表的前半部分和反转后的后半部分 ListNode left = A; // 前半部分的头节点 ListNode right = prev; // 反转后的后半部分的头节点 while (left != null && right != null) { if (left.val != right.val) { return false; // 如果节点的值不相等,则链表不是回文结构 } left = left.next; right = right.next; } return true; // 所有节点的值都相等,链表是回文结构 }
7、给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null
。
链表的优缺点
链表相比于数组具有以下特点和优势:
- 动态性:链表的长度可以根据需要动态地增长或缩小,不需要预先定义大小。
- 插入和删除操作效率高:由于链表的节点之间通过指针连接,插入和删除节点的操作只需要改变指针的指向,时间复杂度为O(1)。
- 空间利用效率高:链表节点在内存中分散存储,不需要连续的内存空间,可以更灵活地利用内存。
- 链表长度没有固定限制:链表的长度可以根据需要动态调整,不受固定大小的限制。
然而,链表也有一些缺点:
访问效率较低:链表中的节点不是连续存储的,访问特定位置的节点需要从头节点开始遍历,时间复杂度为O(n),其中n为链表长度。
额外的存储空间:链表中的每个节点都需要额外的指针来指向下一个节点(以及前一个节点,对于双向链表),因此需要额外的存储空间。
综上,链表适用于需要频繁插入和删除节点的场景,而不太关注访问效率。我们还是需要根据具体的应用场景和需求,选择合适的数据结构(如数组或链表),这是很重要的。