链表的基本概念
在大多数语言中,数组的大小是固定的,从数组的起点或中间插入或移除项的成本很高,因为需要移动元素。(比如之前我们实现的栈和队列)
链表是存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需的元素。
链表的基本方法
push(element):向链表尾部添加一个新元素。如下图:
insert(element, position):向链表的特定位置插入一个新元素。
插入头部如下:
插入其他地方如下:
getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回undefined。
remove(element):从链表中移除一个元素。如下图:
indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
removeAt(position):从链表的特定位置移除一个元素。
isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0 则返回false。
size():返回链表包含的元素个数,与数组的length 属性类似。
toString():返回表示整个链表的字符串。由于列表项使用了Node 类,就需要重写继承自JavaScript 对象默认的toString方法,让其只输出元素的值。
链表结构的自我实现
// 单向链表
class Node{
// 每一个节点的结构
constructor(value) {
this.value = value // 数据域
this.next = undefined // 指针域
}
}
class LinkedList{
constructor() {
this.count = 0 // 计数器
this.head = undefined // 链头,起始节点
}
push(value){ // 在尾部新增一个值
let node = new Node(value) // 实例生成节点对象
let current // 当前遍历所处的节点
if(!this.head){
this.head = node
// 如果说当前链表里是空的,就说明head也是空的,那就直接把新增的节点设置为链表的第一个
}else{ // 如果链表第一个不为空,那我们就往后面数
current = this.head // 链表头为当前已经遍历的节点(如果遇到不好理解的地方,画图)
while(current.next){ // 如果当前节点的指针域所指向的地方不为空的时候,说明它还不是最后一个节点,则循环的条件依旧为true,就还能继续循环遍历,如果条件为false,就不会进入该循环
current = current.next // 只要不是最后一个节点,就一直把当前所遍历到的那个节点的指向,指向下一个
}
current.next = node // 如果遍历到了最后一个节点,跳出了循环,那么就把当前的指针域指向这被遍历到的最后一个节点
}
this.count++ // 每当在尾部添加完一个新值之后,计数器加一
return linkData
}
// 怎么才算删除,当没有指针指向该节点,它就被删除了,所以我们只需要找到被删除节点i的前一个节点i-1,将前一个节点i-1的指针 指向 被删除节点 之后的一个节点i+1 即可:就是(i-1)=> (i+1),i就不见了
removeAt(index){ // 删除链表上指定下标的节点
if(index >= 0 && index < this.count){ // 如果要删除一个指定的数据,必须满足下标大于等于0且小于链表长度这两个条件
let current = this.head // 默认将链头作为当前所处节点位置
let beRemoved // 被删除的
if(index = 0){ // 如果删除的是第0个,链头
beRemoved = current // 由上可知current是this.head,则需要被删除的就是this.head
this.head = current.next // 链头指向下一个数据,那么原来的链头数据就被删除了
}else{ // 下面这一步for循环非常难理解,可以试着画图并代入具体的数值0 1 2 3 4来理解
for(let i = 1; i < index; i++){ // 为啥从1开始,因为刚刚已经排除了第0位
current = current.next // 当循环结束时,就遍历到了要被删除节点的前一个节点,index是要被删除的节点的下标,因为i<index,所以i此时的值就是index-1
}
beRemoved = current.next // 由上面for循环可知,current是代表待删除节点的前一个节点,则current.next就是代表需要被删除的节点
current.next = current.next.next // 当前节点的前一个节点的指针域 指向 当前节点的下一个节点,那么当前节点就被删除了
}
this.count-- // 删除之后,相应的计数器减1
return beRemoved // 返回被删掉的节点的值
}
}
removeValue(value){ // 删除链表上指定值的节点
let current = this.head
let pre = null // 被删除节点的前一个节点
// 分情况,当链表头就是要删除的值时或者链表其他节点是要被删除的值的时候
if(current.value === value){ // 如果输入的值与链头的值相等
this.head = current.next // 就把链表头指向原来链表头的下一个节点
}else{ // 否则
for(let i = 1; i < this.count; i++){ // 循环遍历每一个节点,当前节点的值不是要删除的就一直寻找下一个节点的值
pre = current // 将i-1保存到变量pre(当前的current是this.head,当前节点成为上一个节点)
current = current.next // 需要被删除的节点i(当前节点的下一个节点成为当前节点)
if(current.value === value){ // 当需要被删除值与当前节点的值相同时
pre.next = current.next // 前一个节点(i-1)的指针域 指向 需要被删除的节点(i)的指针域所指向的节点(i+1)
break // break删除符合条件的第一个值,如果没写break就是继续循环,会删除符合条件的所有值
}
}
}
this.count-- // 计数器减1
return `已经删除符合条件的第一个值${value}`
}
pop(){ // 模拟pop功能,删除最后一个
return this.removeAt(this.count - 1)
}
indexOf(value){ // 查询一个值在链表里面的位置,返回该值的位置下标
let current = this.head
if(value === current.value){ // 三个等号全等,数据类型也要相同
return 0
}
// 如果不是第0位,那么就进入下面这个循环
for(let i = 1; i < this.count; i++){
current = current.next // 不是第0位,那么就一直循环往后挪
if(value === current.value){
return i
}
}
return undefined // 如果既不是if里面的,也不是for循环里面的,那就说明找不到,找不到就返回undefined
}
getNodeAt(index){ // 获取指定下标位置上的值
if(index >=0 && index < this.count){
let current = this.head
if(index === 0){
return current
}else{
for(let i = 1; i < index; i++){
current = current.next // 当循环结束时,i是被指定下标节点的前一个节点的下标
}
return current.next
}
}
}
// 假如要在第i个下标插入一个新的节点,那么就是i-1指向新的节点,新的节点指向i,这样新的节点就插入了
insert(value,index){ // 指定下标位置插入新节点
let node = new Node(value) // 实例化生成节点
if(index >= 0 && index <= this.count){ // 输入的索引值在链表长度内
let current = this.head // 先把原来开头的值保存下来
if(index === 0){ // 插入开头
this.head = node
this.head.next = current // 把新的开头值的指针域 指向 原先开头的值
}else{
for(let i = 1; i < index; i++){
current = current.next // 当循环结束时,插入新节点的位置 的 前一个节点 等号左边的current变量是i-1
}
let nextEle = current.next // nextEle保存原来的i
current.next = node // i-1的指针域指向新插入的节点
node.next = nextEle // 新插入的节点的指针域指向原来的i
this.count++
}
}else{ // 输入的索引值超出链表原本的长度
throw new Error("索引值错误")
}
}
unshift(value){ // 在链表头部插入新节点
this.insert(value, 0)
}
isEmpty(){
return !this.count
}
size(){
return this.count
}
getHead(){ // 获取当前链表头的值
return this.head.value
}
toString(){ // 转换成字符串形式输出
let objString = ""
let current = this.head
if(!current){ // 如果链表头不存在
return "" // 返回空
}else{
do{ // 至少会先执行一次do里面的循环语句,然后再判断while的条件是否还满足循环
objString = `${objString},${current.value}`
current = current.next
}while(current) // 只要还有当前值,就一直进行do里面的循环
return objString.slice(1) // 去掉第一次拼接后的逗号
}
}
}
let linkData = new LinkedList()
linkData.push(0)
linkData.push(1)
linkData.push(2)
linkData.push(3)
linkData.push(4)
双向链表的基本概念
双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素,如下图所示:
// 双向链表
class DBNode{
// 每一个节点的结构
constructor(value) {
this.value = value // 数据域
this.next = undefined // 下一个指针域
this.pre = undefined // 上一个指针域
}
}
class LinkedList{
constructor() {
this.count = 0 // 计数器
this.head = undefined // 链头,起始节点
}
push(value){ // 在尾部新增一个值
let node = new DBNode(value) // 实例生成节点对象
let pre = this.head
let current // 当前遍历的节点
if(!this.head){
this.head = node
// 如果说当前链表里是空的,就说明head也是空的,那就直接把新增的设置为链表的第一个
}else{ // (这里需要结合上面双向链表的图会更好理解)如果链表第一个不为空,那我们就往后面数
current = this.head // 链表头为当前已经遍历的节点
while(current.next){ // 如果当前节点的指针域所指向的地方不为空的时候,说明它还不是最后一个节点,就继续循环遍历
current = current.next // 只要不是最后一个节点,就一直把当前所遍历到的那个节点的指针域,指向下一个节点
pre = current // 然后当前节点变成上一个节点pre,只有这样current才能不断循环变成后面一个节点
}
current.next = node // 如果遍历到了最后一个节点,跳出了循环,那么就把当前的最后一个节点的指针域指向新增的节点node
node.pre = pre // 然后新增节点node的前一个指针域指向上一个节点pre(看上面两行代码)
}
this.count++ // 当在尾部添加完一个新值之后,计数器加一
}
removeAt(index){ // 删除链表上面的第几个数据
if(index >= 0 && index < this.count){ // 如果要删除一个指定的数据,必须满足下标大于等于0且小于链表长度这两个条件
let current = this.head
let beRemoved // 被删除的
if(index = 0){ // 如果删除的是第0个,链头
beRemoved = current
this.head = current.next // 链头指向下一个数据,那么原来的链头数据就被删除了
this.head.pre = undefined
// 当原来链表头被删除,原来的第1位成为新的链表头,但原来的第1位的pre是有值的指向被删除的那个节点,所以需要将原来的指向undefined清空
}else{ // 这一步for循环可以试着画图并代入具体的数值0 1 2 3 4来理解
for(let i = 1; i < index; i++){ // 为啥从1开始,因为刚刚已经排除了第0位
current = current.next // 当循环结束时,要被删除节点的前一个节点,i-1
}
beRemoved = current.next // 待删除的节点,i
current.next.next.pre = current
// 由上一行代码可知current.next指向被删除的节点,那么current就是被删除节点的前一个节点,
// 所以被删除节点的下一个节点的上一个指针域指向被删除节点的前一个节点current
current.next = current.next.next // 当前节点的前一个节点的指针域 指向 当前节点的下一个节点,那么当前节点就被删除了
}
this.count--
return beRemoved
}
}
removeValue(value){ // 删除链表中指定值的节点
let current = this.head
let pre = null // 被删除节点的前一个节点
if(current.value === value){ // 如果输入的值与链头的值相等
this.head = current.next
this.head.pre = undefined // 同上面removeAt的道理一样
}else{
for(let i =1; i < this.count; i++){
pre = current // 将i-1保存到变量pre
current = current.next // 需要被删除的节点i
if(current.value === value){ // 当被删除的值与节点值相同时
pre.next = current.next // 前一个节点(i-1)的指针域 指向 需要被删除的节点(i)的指针域所指向的节点(i+1)
current.next.pre = pre
// 注意前三行代码写了current变量保存了需要被删除的节点i,被删除节点的下一个节点的前一个指针域指向被删除节点的前一个节点pre(i-1)
break // 删除符合条件的第一个值,如果没写break就是继续循环,删除符合条件的所有值
}
}
}
this.count--
}
pop(){ // 模拟pop功能,删除最后一个
return this.removeAt(this.count - 1)
}
indexOf(value){ // 查询一个值在链表里面的位置,返回该值的位置下标
let current = this.head
if(value === current.value){ // 三个等号全等,数据类型也要相同
return 0
}
// 如果不是第0位,那么就进入下面这个循环
for(let i = 1; i < this.count; i++){
current = current.next // 不是第0位,那么就一直循环往后挪
if(value === current.value){
return i
}
}
return undefined // 如果既不是if里面的,也不是for循环里面的,那就说明找不到,找不到就返回undefined
}
getNodeAt(index){ // 获取指定位置的值
if(index >=0 && index < this.count){
let current = this.head
if(index === 0){
return current
}else{
for(let i =1; i < index; i++){
current = current.next // 当循环结束时,被剔除的节点的前一个节点
}
return current.next
}
}
}
insert(value,index){ // 指定位置插入新节点
let node = new DBNode(value)
if(index >= 0 && index <= this.count){ // 输入的索引值在链表长度内
let current = this.head // 先把原来开头的值保存下来
if(index === 0){ // 插入开头
this.head = node
this.head.next = current // 把新的开头值的指针域 指向 原先开头的值
current.pre = this.head // 原先开头的值的前一个指针域指向最新的开头值
}else{
for(let i = 1; i < index; i++){
current = current.next // 当循环结束时,插入新节点的位置 的 前一个节点 等号左边的current变量是i-1
}
let nextEle = current.next // nextEle保存原来的i
current.next = node // i-1的指针域指向新插入的节点
node.next = nextEle // 新插入的节点的指针域指向原来的i
node.pre = current
// 当插入第1位的时候,i=1,1不小于1所以不走for循环,所以current还是this.head,新插入的node节点的前一个指针域指向this.head
nextEle.pre = node
// 原先在第1位的节点nextEle的前一个指针域pre指向最新插入的node节点
this.count++
}
}else{ // 输入的索引值超出链表长度
throw new Error("索引值错误")
}
}
isEmpty(){
return !this.count
}
size(){
return this.count
}
getHead(){
return this.head.value
}
toString(){
let objString = ""
let current = this.head
if(!current){ // 如果链表头不存在
return "" // 返回空
}else{
do{ // 至少会先执行一次do里面的循环语句,然后再判断while时候满足循环条件
objString = `${objString},${current.value}`
current = current.next
}while(current) // 只要还有当前值,就一直进行do里面的循环
return objString.slice(1)
}
}
}
let linkData = new LinkedList()