何为链表
在计算机科学的世界里,有一种神奇的数据结构,它以一种独特的方式将数据节点连接在一起,创造出无限的可能性。它就是链表。从头到尾,这篇文章将带您踏上一场探索链表的奇妙之旅,揭开它隐藏的秘密,探寻它所带来的无限潜力。无论您是一名编程新手还是经验丰富的软件工程师,本文将为您详细解说链表的工作原理、优势与应用场景。准备好迎接数据的律动和魅力了吗?让我们一起跳进这个充满惊喜的链表世界吧!
链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:一个是存储数据的字段(通常称为"数据域"),另一个是指向下一个节点的引用(通常称为"指针"或"引用")。这样的设计使得链表中的节点在内存中并不需要连续的存储空间,它们可以分布在内存的任意位置,通过指针相互连接起来。
ini
function ListNode(val) {
this.val = val;
this.next = null
}
let node1 = new ListNode(1)
node1.next = new ListNode(2)
node1.next.next = new ListNode(3)
这是一个简单的链表。该链表的头节点是node1
,它的值为1,下一个节点是2,再下一个节点是3。而
链表的增删查改
在JS中,我们可以使用对象的方式来模拟链表。比如下面这个ListNode
类:
kotlin
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
增:头部插入,尾部插入,指定位置插入。
- 头部插入
javascript
function addAtHead(head, val) {
const newHead = new ListNode(val); // 创建一个新的节点,值为val
newHead.next = head; // 将新节点的next指向原来的头节点
return newHead; // 返回新的头节点
}
在这个函数内部,我们首先创建了一个新的节点newHead
,它的值就是我们要插入的val
。然后,将新节点的next
指向原来的头节点,这样就把原来的链表整体往后移动了一位,而新节点成为了新的头节点。
最后,返回新的头节点即可。
- 尾部插入
ini
function addAtTail(head, val) {
const newTail = new ListNode(val); // 创建一个新的节点,值为val
if (!head) { // 如果原链表为空,则直接返回新节点作为头节点
return newTail;
}
let cur = head;
while (cur.next) { // 找到链表的尾节点
cur = cur.next;
}
cur.next = newTail; // 将新节点连接到尾节点的next
return head; // 返回头节点
}
在这个函数内部,我们首先创建了一个新的节点newTail
,它的值就是我们要插入的val
。然后,我们判断原链表是否为空。如果为空,说明原链表只有一个节点或者没有节点,此时直接将新节点作为头节点返回即可。
如果原链表不为空,我们需要找到原链表的尾节点。我们使用一个指针cur
来遍历链表,直到找到尾节点,即cur.next
为null
。然后,将新节点newTail
连接到尾节点的next
,即cur.next = newTail
。
最后,返回头节点即可。
- 指定位置插入
ini
function addAtIndex(head, index, val) {
if (index < 0) { // 如果index小于0,直接插入到头部
return addAtHead(head, val);
}
const newNode = new ListNode(val); // 创建新节点
if (index === 0) { // 如果index等于0,直接插入到头部
newNode.next = head;
return newNode;
}
let cur = head;
for (let i = 0; i < index - 1; i++) { // 找到要插入位置的前一个节点
if (!cur) { // 如果cur为null,说明已经到达链表末尾,无法插入
return head;
}
cur = cur.next;
}
if (!cur) { // 如果cur为null,说明已经到达链表末尾,无法插入
return head;
}
newNode.next = cur.next; // 将新节点的next指向cur的next
cur.next = newNode; // 将cur的next指向新节点
return head;
}
在这个函数内部,首先判断了插入位置index
的情况。如果index
小于0,就直接调用addAtHead
方法将新节点插入到头部;如果index
等于0,也直接将新节点插入到头部;否则,需要找到要插入位置的前一个节点,然后进行插入操作。
具体的插入过程如下:
- 首先创建一个新的节点
newNode
,它的值为val
。 - 如果
index
为0,直接将newNode
的next
指向head
,然后返回newNode
作为新的头节点。 - 如果
index
不为0,使用一个指针cur
来遍历链表,找到第index-1
个节点,即要插入位置的前一个节点。 - 然后将
newNode
的next
指向cur.next
,将cur
的next
指向newNode
。
删:头部删除,尾部删除,指定位置删除。
- 头部删除
javascript
function deleteAtHead(head) {
if (!head) { // 如果链表为空,直接返回null
return null;
}
const newHead = head.next; // 将头节点的下一个节点作为新的头节点
head.next = null; // 将原头节点断开连接
return newHead; // 返回新的头节点
}
在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null
。
如果链表不为空,我们需要进行删除操作。首先将头节点的下一个节点作为新的头节点,即将head.next
赋值给newHead
。
然后将原头节点的next
指针置为null
,断开与后续节点的连接,以确保被删除的头节点不再与链表关联。
最后,返回新的头节点newHead
。
- 尾部删除
javascript
function deleteAtTail(head) {
if (!head) { // 如果链表为空,直接返回null
return null;
}
if (!head.next) { // 如果链表只有一个节点,删除后返回null
return null;
}
let cur = head;
while (cur.next.next) { // 找到倒数第二个节点
cur = cur.next;
}
cur.next = null; // 将倒数第二个节点的next指向null,断开与尾节点的连接
return head; // 返回头节点
}
在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null
。
然后判断链表是否只有一个节点。如果链表只有一个节点,删除后返回null
。
如果链表不为空且节点数大于1,我们需要进行删除操作。首先使用一个指针cur
来遍历链表,找到倒数第二个节点,即要删除的节点的前一个节点。
然后将倒数第二个节点的next
指针置为null
,断开与尾节点的连接,以确保被删除的尾节点不再与链表关联。
最后,返回头节点head
。
- 指定位置删除
ini
function deleteAtPosition(head, position) {
if (!head) { // 如果链表为空,直接返回null
return null;
}
if (position === 1) { // 如果要删除的是头节点,直接返回头节点的下一个节点作为新的头节点
return head.next;
}
let cur = head;
let count = 1;
while (cur && count < position - 1) { // 找到要删除节点的前一个节点
cur = cur.next;
count++;
}
if (!cur || !cur.next) { // 如果找不到要删除的位置或者要删除的节点不存在,直接返回头节点
return head;
}
cur.next = cur.next.next; // 将前一个节点的next指向要删除节点的下一个节点,实现删除操作
return head; // 返回头节点
}
在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null
。
然后判断要删除的节点是否是头节点。如果要删除的是头节点,直接返回头节点的下一个节点作为新的头节点。
如果要删除的节点不是头节点,我们需要进行删除操作。首先使用一个指针cur
来遍历链表,找到要删除节点的前一个节点。
然后根据指定的位置position
,通过循环移动cur
指针,直到它指向要删除节点的前一个节点。
如果无法找到要删除的位置或者要删除的节点不存在(cur
或cur.next
为空),直接返回头节点。
最后,将前一个节点的next
指针指向要删除节点的下一个节点,实现删除操作。
查:查找节点
ini
function findNode(head, value) {
let cur = head;
while (cur) {
if (cur.value === value) { // 如果当前节点的值等于目标值,返回当前节点
return cur;
}
cur = cur.next; // 否则继续遍历下一个节点
}
return null; // 遍历完整个链表都没有找到目标值,返回null
}
在这个函数内部,我们使用一个指针cur
来遍历链表。
在每次循环中,我们首先判断当前节点cur
的值是否等于目标值value
。如果相等,则表示找到了目标节点,直接返回当前节点cur
。
如果当前节点的值不等于目标值,我们将指针cur
移动到下一个节点,继续遍历。
如果遍历完整个链表都没有找到目标值,即指针cur
变为null
,那么表示目标节点不存在于链表中,返回null
。
改:更新节点
ini
function updateNode(head, position, value) {
let cur = head;
let count = 1;
while (cur && count < position) { // 找到要更新的节点
cur = cur.next;
count++;
}
if (!cur) { // 如果要更新的位置超出链表长度,直接返回头节点
return head;
}
cur.value = value; // 更新节点的值
return head; // 返回头节点
}
在这个函数内部,我们使用一个指针cur
来遍历链表,找到要更新的节点。
首先,我们判断链表是否为空。如果链表为空,直接返回头节点。
然后,我们通过循环移动指针cur
,直到它指向要更新的节点的位置。我们使用计数器count
来记录当前节点的位置。
如果要更新的位置超出了链表的长度(即cur
变为null
),则直接返回头节点。
接下来,我们将要更新节点的值修改为给定的新值。 最后,返回头节点。
总结
本文详细介绍了链表的增删改查,相信很多看到这里的不少小伙伴已经有疑惑了,链表和数组的差距怎么这么大,像数组的增删改查方便轻松的多。像链表这样麻烦的数据结构有什么用呢?直接用数组不好吗?
其实不管什么数据结构都有其优点与缺点。二者最大的区别便在于:由于链表中的元素在内存中不必连续存储,因此插入和删除一个元素的时间复杂度为O(1),而数组则需要将后面的元素全部向后移动,时间复杂度为O(n)。
感谢各位读者坚持看完本文,如果文章对你有所帮助,还望点个赞支持一下,感谢。