源码地址:GitHub-算法通关村
一、链表的介绍
1.1 概念
链表(Linked List)是一种基本的数据结构。它是由一系列节点(Node)组成的数据集合,每个节点都包含两个要素:数据(通常称为值或元素)和一个指向下一个节点的引用(指针或链接)。
1.2 特点
链表的特点是数据元素不必在内存中连续存储,而是通过指针相互连接,形成一个动态的数据结构。这使得链表在插入和删除元素时更加高效,因为它不需要像数组一样进行元素的搬移。然而,链表的随机访问效率较低,因为要查找特定位置的元素,必须从头节点开始顺着链表逐个遍历,直到找到目标节点。
1.3 分类
常见的链表类型有单向链表、双向链表和循环链表:
-
单向链表(Singly Linked List):每个节点包含数据和指向下一个节点的指针。链表的最后一个节点指向空(NULL),表示链表的末尾。
-
双向链表(Doubly Linked List):每个节点包含数据,同时具有指向下一个节点和上一个节点的两个指针。这使得在双向链表中可以更方便地进行双向遍历。
-
循环链表(Circular Linked List):在单向或双向链表的基础上,将链表的最后一个节点的指针指向头节点,形成一个循环。这样,链表中没有真正意义上的末尾,可以通过任何节点开始循环遍历。
1.4 应用
链表在许多编程场景中都有广泛的应用。它常用于动态数据结构,如队列、栈以及其他高级数据结构的基础。同时,在一些内存受限或需要频繁插入、删除元素的情况下,链表也是一种优雅的选择。
1.5 缺点
然而,链表也有一些缺点。相对于数组,链表的存储空间消耗更大,因为每个节点都需要额外的指针空间来连接其他节点。而且,由于链表中的元素在内存中不是连续存储的,可能会导致缓存未命中,从而降低访问速度。因此,在特定的问题和需求下,需要权衡链表的优缺点来选择最合适的数据结构。
二、链表的使用
2.1 构造
/**
* 构造链表
*/
static class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
next = null;
}
}
2.2 初始化
/**
* 初始化链表
* @param arr
* @return
*/
private static Node initLinkedList(int[] arr) {
Node head = null; // 头结点
Node cur = null; // 当前结点
for (int i = 0; i < arr.length; i++) {
Node newNode = new Node(arr[i]);
newNode.next = null;
if (i == 0) {
head = newNode;
cur = newNode;
} else {
cur.next = newNode;
cur = newNode;
}
}
return head;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6};
Node head = initLinkedList(arr);
System.out.println(head);
}
2.3 获取链表长度
/**
* 获取链表长度
*
* @param head
* @return
*/
public static int getLength(Node head) {
int length = 0;
Node node = head;
while (node != null) {
length++;
node = node.next;
}
return length;
}
2.4 向链表中插入结点
/**
* 向链表中插入结点
*
* @param head 链表头结点
* @param nodeInsert 待插入的结点
* @param position 插入位置
* @return 新链表的头结点
*/
public static Node insertNode(Node head, Node nodeInsert, int position) {
// 1.先判空,否则报空指针异常
if (head == null) {
return nodeInsert;
}
// 2.越界判断
int size = getLength(head);
if (position < 1 || position > size + 1) {
System.out.println("插入位置越界");
return head;
}
// 在链表开头插入
if (position == 1) {
nodeInsert.next = head;
return nodeInsert;
}
// 遍历寻找待插入结点的前一个结点
Node preNode = head;
int count = 1;
while (count < position - 1) {
count++;
preNode = preNode.next;
}
// 插入结点
nodeInsert.next = preNode.next;
preNode.next = nodeInsert;
return head;
}
以下是需要注意的问题:
-
头节点为空判断:在插入节点之前,进行头节点为空的判断,可以避免空指针异常。
-
越界判断:在插入节点时,通过
getLength()
方法计算链表长度,并进行越界判断。这确保了插入位置的合法性,避免了插入位置超出链表长度的情况。 -
插入位置为1的情况:在代码中,处理了插入位置为1的情况,即在链表开头插入节点。
-
找到待插入节点的前一个节点:在遍历链表查找待插入节点的前一个节点时,需要保证遍历不会超出链表长度,并且找到正确的位置。
-
插入节点操作:nodeInsert.next = preNode.next; preNode.next = nodeInsert;
2.5 删除链表结点
/**
* 删除链表结点
*
* @param head 待删链表头结点
* @param position 删除结点位置
* @return 新链表头结点
*/
public static Node deleteNode(Node head, int position) {
// 1.先判空
if (head == null) {
return null;
}
// 2.判断位置参数
int size = getLength(head);
if (position > size || position < 1) {
System.out.println("删除位置有误");
return head;
}
// 头删法
if (position == 1) {
return head.next;
}
// 遍历寻找待删除结点的前一个结点
Node preNode = head;
int count = 1;
while (count < position - 1) {
count++;
preNode = preNode.next;
}
// 删除结点
preNode.next = preNode.next.next;
return head;
}
以下是需要注意的问题:
-
空指针检查:函数首先会检查输入的链表头结点"head"是否为空。这是为了避免在对链表进行操作时出现空指针异常。
-
位置有效性检查:函数会检查给定的删除位置"position"是否有效。删除位置应该在链表大小的范围内(从1到链表大小)。如果删除位置超出这个范围,函数会打印一条消息指示删除位置无效,并返回原始头结点,不做任何操作。
-
头部删除:如果删除位置是链表的头部(position为1),函数会直接返回头结点的下一个结点,从而实现删除头结点的操作。
-
寻找待删除结点的前一个结点:对于非头部的删除操作,函数会遍历链表,寻找待删除结点的前一个结点,以便在删除时将前一个结点的next指针指向待删除结点的下一个结点,从而完成删除操作。
2.6 输出链表
/**
* 输出链表
*
* @param head
* @return
*/
public static String printLinkedList(Node head) {
StringBuffer sb = new StringBuffer();
while (head != null) {
sb.append(head.val);
head = head.next;
if (head != null) {
sb.append("->");
}
}
return sb.toString();
}