本文详细讲解 JavaScript 中链表数据结构的实现,涵盖单向链表与双向链表的核心概念及代码实现。内容涉及节点构造函数、append、insert、remove 等关键方法的逻辑推导与具体编码,对比数组优势,并提供完整的单向及双向链表操作示例,帮助开发者掌握动态内存管理与指针操作技巧。
链表简介
链表简介
链表是一种基础的线性数据结构,其存储方式不同于数组的连续内存分配。在链表中,数据并非按物理顺序紧密排列,而是通过每个节点中存储的指向下一个节点的指针(引用)来建立逻辑连接。这种结构允许程序灵活利用内存空间,避免了数组需要提前声明固定长度所带来的资源浪费或溢出问题,特别适合动态数据管理场景。
单向链表
单向链表
作为链表最基础的形态,单向链表中的每个节点由两部分组成:一部分存储实际数据元素,另一部分存储指向后继节点的指针。链表的最后一个节点其指针域通常指向空值(NULL),标志着链表的结束。这种结构直观地体现了数据的前后关联关系。
JavaScipt中单向链表的实现
JavaScipt中单向链表的实现
实现单向链表的第一步是定义其构造函数,用于初始化链表的基本状态,包括头节点引用和长度计数。
/**
* 单向链表构造函数
*/
function LinkedList() {
/**
* 单向链表中节点的构造函数
* @param {Any} element 要传入链表的节点
*/
var Node = function(element) {
this.element = element;
//下个节点的地址
this.next = null;
}
//单向链表的长度
var length = 0;
//单向链表的头结点,初始化为NULL
var head = null;
}
相较于栈或队列等线性结构,单向链表的构造函数实现更为复杂,因为它需要维护节点间的指针关系。一个完整的单向链表类通常需要提供以下核心操作方法:
- append(element): 添加元素到链表尾部
- insert(position,element): 向单向链表中某个位置插入元素
- indexOf(element): 寻找某个元素在单向链表的位置
- remove(element): 移除给定的元素
- removeAt(position): 移除单向链表中某个位置的元素
- getHead(): 获取单向链表的头部
- isAmpty(): 检查单向链表是否为空,为空则返回true
- toString(): 将链表所有内容以字符串输出
- size(): 返回单向链表长度
append方法:
该方法用于将新元素追加到链表的末尾。实现逻辑首先创建包含新数据的新节点,若链表为空则直接将该节点设为头节点;否则,遍历链表至最后一个节点(其 next 为 null),将最后一个节点的 next 指向新节点,并更新链表长度。
/**
* 向单向链表尾部添加元素
* @param {Any} element 要加入链表的节点
*/
this.append = function(element) {
var node = new Node(element);
var current;
if (head == null) {
head = node;
} else {
// 当前项等于链表头部元素.
// while循环到最后一个,从而将该节点加入链表尾部。
current = head;
// 当next为null时,判定为false。退出循环。
while (current.next) {
current = current.next;
}
current.next = node;
}
length++;
};
insert方法:
该方法用于在指定索引位置插入新元素。首先验证位置合法性(0 到 length 之间)。若插入位置为 0,则新节点的 next 指向当前头节点,并将头节点更新为新节点;否则,通过循环找到目标位置的前驱节点和后继节点,调整指针使新节点插入其中,最后增加长度并返回成功标识。
/**
* 向单向链表中插入某个元素
* @param {Number} position 要插入的位置
* @param {Any} element 要插入的元素
* @return {Boolean} 插入成功返回true,失败返回false
*/
this.insert = function(position, element) {
if (position >= 0 && position <= length) {
var node = new Node(element);
var current = head;
var previous;
var index = 0;
if (position == 0) {
node.next = current;
head = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
}
length++;
return true;
} else {
return false;
}
};
indexOf方法:
该方法用于查找指定元素在链表中的索引位置。从头部开始遍历,若当前节点的 element 与目标元素严格相等(===),则返回当前索引;若遍历结束未找到,则返回 -1。
/**
* 寻找某个元素在单向链表中的位置
* @param {Any} element 要寻找的元素
* @return {Number} 返回值>=0则代表找到相应位置
*/
this.indexOf = function(element) {
var current = head;
var index = -1;
while (current) {
if (element === current.element) {
return index;
}
index++;
current = current.next;
}
return -1;
};
remove方法:
该方法用于移除链表中第一个匹配指定值的元素。其实现依赖于 indexOf 方法获取目标元素的索引,随后调用 removeAt 方法执行实际的移除操作并返回被移除的元素值。
/**
* 移除给定的元素
* @param {Any} element 要移除的元素
* @return {Number} 返回值>=0表示移除成功
*/
this.remove = function(element) {
var index = this.indexOf(element);
return this.removeAt(index);
};
removeAt方法:
该方法用于移除指定索引位置的节点。首先验证索引范围。若移除的是头节点,则将 head 指针直接指向原 head 的 next 节点;否则,通过循环找到目标节点的前驱节点,将前驱节点的 next 指向目标节点的 next,从而跳过目标节点。操作完成后长度减 1,并返回被移除节点的数据。
/**
* 移除单向链表中某一个元素
* @param {Number} position 要移除元素的位置
* @return {Any} 移除成功返回被移除的元素,不成功则返回NULL
*/
this.removeAt = function(position) {
if (position > -1 && position < length) {
var current = head;
var previous;
var index = 0;
if (position == 0) {
// 因为之前head指向第一个元素,现在把head修改为指向第二个元素。
// 核心概念在于链表前后全靠指针链接,而非数组一般。
// 所以只需要改变head的元素。
head = current.next;
} else {
while (index++ < position) {
// previous指要操作元素位置之前的那个元素,current表示之后的那个元素。
previous = current;
current = current.next;
}
previous.next = current.next;
}
length--;
return current.element;
} else {
return null;
}
};
getHead方法:
该方法用于获取链表的头节点引用,直接返回内部维护的 head 变量。
/**
* 获取单向链表的头部
* @return {Any} 单向链表的头部
*/
this.getHead = function() {
return head;
}
isAmpty、toString、size方法
这些辅助方法分别用于检查链表是否为空(判断 head 是否为 null)、将链表内容转换为字符串表示(通常遍历节点拼接数据)以及返回当前链表的节点总数。
/**
* 判断单向链表是否为空
* @return {Boolean} 为空则返回true,不为空则返回false
*/
this.isAmpty = function() {
return length === 0
};
/**
* 将链表所有内容以字符串输出
* @return {String} 要输出的字符串
*/
this.toString = function() {
var current = head;
var string = '';
while (current) {
string += current.element;
current = current.next;
}
return string;
};
/**
* 返回单向链表长度
* @return {Number} 单向链表的长度
*/
this.size = function() {
return length;
};
双向链表
双向链表
双向链表是对单向链表的扩展,其节点不仅包含指向后继节点的指针,还包含指向前驱节点的指针。这种双向链接结构使得从任意节点向前或向后遍历成为可能,增加了操作的灵活性,但同时也增加了内存开销和指针管理的复杂度。
JavaScipt中双向链表的实现
JavaScipt中双向链表的实现
双向链表的构造函数同样需要初始化头节点、尾节点(可选,用于优化尾部操作)以及长度变量,并定义节点结构以支持双向指针。
/**
* 双向链表的构造函数
*/
function DoublyLinkedList() {
/**
* 双向链表中节点的构造函数
* @param {Any} element 要传入链表的元素
*/
var Node = function(element) {
this.element = element;
this.prev = null;
this.next = null;
}
//双向链表的长度
var length = 0;
//双向链表的头结点,初始化为NULL
var head = null;
//双向链表的尾结点,初始化为NULL
var tail = null;
}
双向链表需要提供以下核心方法以支持双向遍历和操作:
- append(element): 添加元素到双向链表尾部
- insert(position,element): 向双向链表中某个位置插入元素
- removeAt(position): 移除双向链表中某个位置的元素
- showHead(): 获取双向链表的头部
- showLength(): 获取双向链表长度
- showTail(): 获取双向链表的尾部
append方法:
该方法用于在双向链表尾部添加元素。创建新节点后,若链表为空,则初始化 head 和 tail;否则,将当前尾节点的 next 指向新节点,并将新节点的 prev 指向当前尾节点,最后更新 tail 为新节点。
/**
* 向链表尾部添加元素
* @param {Any} element 要加入链表的节点
* @return {Any} 加入链表的节点
*/
this.append = function(element) {
var node = new Node(element);
if (head === null) {
head = node;
tail = node;
} else {
var previous;
var current = head;
while (current.next) {
current = current.next;
}
current.next = node;
node.prev = current;
tail = node;
}
length++;
return node;
};
insert方法:
该方法用于在指定位置插入元素。验证位置后,若插入头部,需特殊处理 head 和 tail 指针;若在中间或尾部,则通过遍历找到目标位置的前驱和后继节点,调整四个指针(前驱的 next、后继的 prev、新节点的 prev 和 next)以完成插入,并更新长度。
/**
* 向链表中插入某个元素
* @param {Number} position 要插入的位置
* @return {Boolean} 插入成功返回true,失败返回false
*/
this.insert = function(position, element) {
if (position >= 0 && position <= length) {
var node = new Node(element);
var index = 0;
var previous;
var current = head;
if (position === 0) {
if (head === null) {
head = node;
tail = node;
} else {
current.prev = node;
node.next = current;
head = node;
}
} else if (position === length) {
current = tail;
current.next = node;
node.prev = current;
tail = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = node;
node.prev = previous;
current.prev = node;
node.next = current;
}
length++;
return true;
} else {
return false;
}
};
removeAt方法:
该方法用于移除指定位置的节点。验证索引后,若移除的是头节点,需更新 head;若移除的是尾节点,需更新 tail。对于中间节点,需调整前驱节点的 next 和后继节点的 prev 以跳过被移除节点,最后更新长度并返回移除的数据。
/**
* 移除链表中某一个元素
* @param {Number} position 要移除元素的位置
* @return {Any} 移除成功返回被移除的元素,不成功则返回false
*/
this.removeAt = function(position) {
if (position > -1 && position < length) {
var current = head;
var index = 0;
var previous;
if (position === 0) {
head = current.next;
if (length === 1) {
tail = null;
head.prev = null;
}
} else if (position === length - 1) {
current = tail;
tail = current.prev;
tail.next = null;
} else {
while (index++ < position) {
previous = current.prev;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
length--;
return current.element;
} else {
return false;
}
};
showHead、showLength、showTail方法
这些方法分别用于返回头节点引用、当前链表长度以及尾节点引用,辅助外部程序获取链表状态。
/**
* 获取链表的头部
* @return {Any} 链表的头部
*/
this.showHead = function() {
return head;
};
/**
* 获取链表长度
* @return {Number} 链表长度
*/
this.showLength = function() {
return length;
};
/**
* 获取链表尾部
* @return {Any} 链表尾部
*/
this.showTail = function() {
return tail;
};
感想
感想
在实现链表的过程中,先根据需求编写代码,随后与标准教材或参考实现进行对比,能发现自身逻辑中的潜在漏洞和复杂性。实际编码中容易忽略边界条件(如空链表、单节点链表)和指针指向的细节,导致逻辑混乱。通过对比学习,可以更深刻地理解指针操作的正确性和数据结构设计的严谨性。