一. 链表的定义
链表是一种线性数据结构,由一系列节点组成。每个节点包含两个部分:
- 数据域(Data):存储节点的数据。
- 指针域(Pointer):存储指向下一个节点的地址。
链表的第一个节点称为头节点(Head),最后一个节点的指针域指向空(NULL),表示链表的结束。
二. 链表的结构
1) 单向 / 双向
2) 带头 / 不带头
3) 循环 / 非循环
链表种类丰富多样 重点掌握 单向不带头非循环 链表 可作为其他数据结构的子结构,如 哈希桶、图的邻接表等 笔试常考
三. 实现链表
1) 节点类:
定义链表节点类,每个节点包含数据和指向下一个节点的指针。
public class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
2) 链表类:
用于实现链表的功能
public class MySingleList {
private ListNode head;
static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
public ListNode cur; // 临时头节点
}
插入节点:
插入节点 图示
// 在链表的末尾插入一个新节点
void append(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
}
usedSize++;
}
// 在链表的开头插入一个新节点
void prepend(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
usedSize++;
}
// 在指定位置插入一个新节点
void insertAt(int index, int data) {
if (index < 0 || index > usedSize) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
Node newNode = new Node(data);
if (index == 0) {
newNode.next = head;
head = newNode;
} else {
Node current = head;
for (int i = 0; i < index - 1; i++) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
}
usedSize++;
}
删除节点:
// 删除指定位置的节点
void deleteAt(int index) {
if (index < 0 || index >= usedSize) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
if (index == 0) {
head = head.next;
} else {
Node current = head;
for (int i = 0; i < index - 1; i++) {
current = current.next;
}
current.next = current.next.next;
}
usedSize--;
}
查找节点:
// 查询指定位置的节点
Node getNodeAt(int index) {
if (index < 0 || index >= usedSize) {
throw new IndexOutOfBoundsException("Index out of bounds");
}
Node current = head;
for (int i = 0; i < index; i++) {
current = current.next;
}
return current;
}
四. 链表OJ 实战 !
1) 开胃小菜 删除所有值域为val的节点
力扣链接: . - 力扣(LeetCode)
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head!= null && head.val == val){
head = head.next;
}
ListNode cur = head;
while(cur != null && cur.next != null){
if(cur.next.val == val){
cur.next = cur.next.next;
}else{
cur = cur.next;
}
}
return head;
}
}
2) 反转链表 重要程度 五颗星 !!!
力扣链接: . - 力扣(LeetCode)
// 反转链表
public ListNode reverseList(ListNode head) {
// 定义两个指针,pre初始化为null,用于存储反转后的链表
ListNode pre = null;
// cur初始化为head,表示当前处理的节点
ListNode cur = head;
// 循环直到cur为null,表示已经处理完所有节点
while(cur != null){
// temp临时存储cur的下一个节点,因为接下来要修改cur.next
ListNode temp = cur.next;
// 将cur的next指针指向pre,实现反转
cur.next = pre;
// pre和cur都前进一步
pre = cur; // pre移动到cur的位置
cur = temp; // cur移动到下一个待处理的节点
}
// 当cur为null时,pre就是新链表的头节点
return pre;
}
思路: 在遍历链表时逐步反转每个节点的指针方向
3) 快慢指针算法
快慢指针: 处理环形链表或数组非常有用
其实不单单是环形链表或者是数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。
力扣链接: 876. 链表的中间结点 - 力扣(LeetCode)
public ListNode middleNode(ListNode head) {
// 定义快慢指针,都初始化为头节点head
ListNode fast = head;
ListNode slow = head;
// 循环条件,快指针fast及其next不为null
while (fast != null && fast.next != null) {
// 快指针fast每次向前移动两步
fast = fast.next.next;
// 慢指针slow每次向前移动一步
slow = slow.next;
}
// 当快指针到达链表末尾时,慢指针正好到达中间节点
return slow;
}
原理 : 因为fast的速度是 slow的速度的二倍, 而fast走完,slow 必然在中间位置.
4) 合并两个有序链表:
思路 : 使用 虚拟节点 为了简化头节点的处理逻辑
比较大小 插入新链表
如果其中一个链表的节点全部被插入到新链表中,如果另一个链表还有剩余节点, 直 接将这些剩余节点链接到新链表的末尾。
记得最后返回虚拟节点的下一个节点.
力扣链接: . - 力扣(LeetCode)
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(-1);//虚拟节点
// 创建一个指针cur,用来遍历并构建新的合并后的链表,初始指向哑节点
ListNode cur = dummy;
// 遍历链表直到其中一个为空
while(list1 != null && list2 != null){
if(list1.val > list2.val){
cur.next = list2;
list2 = list2.next;
}else{
cur.next = list1;
list1 = list1.next;
}
cur = cur.next;
}
// 如果其中一个链表先遍历完,将另一个链表的剩余部分连接到新链表的末尾
cur.next = (list1 == null) ? list2:list1;
return dummy.next; //返回虚拟节点的下一个节点
}
五. ArrayList和LinkedList的区别
- 数组: 适用于需要快速访问和固定大小的情况。
- 链表: 适用于频繁插入和删除、且大小动态变化的情况。
六. 总结
链表作为数据结构中的佼佼者,凸显了其在动态数据操作场景下的独特价值,特别是对于需要频繁执行插入和删除操作的应用来说,更是不可或缺。掌握链表的基础操作,包括但不限于节点的创建、插入、删除及遍历,以及进阶技巧如链表的反转、合并操作,以及利用快慢指针解决复杂链表问题,是深化链表理解和应用实践的关键步骤。在链表与数组的权衡选择上,认识到两者各自的强项——数组的快速随机访问与链表的高效动态修改能力,是根据具体需求制定数据结构策略的先决条件。总而言之,链表的灵活高效使其在众多计算领域内占有一席之地,而深入探索其特性和应用,则是对每一位技术探索者的智慧挑战与能力提升之旅。