数据结构与算法
1.链表
相比于之前的数组,链表是一种存储空间不连续的结构。数组的存储是连续的,但是它在移动或者是删除元素时,它后面的元素都要移动相应的位置,所以当数据量比较大时还是比较耗费资源的。链表则是存储空间不连续的一种数据结构,类似于火车,由一个一个节点组成,每个节点包含着指向下一个节点的地址以及数据,当删除或添加时只需要修改节点中存储的引用即可。
链表结构
//节点数据,包含数据以及下一个节点的地址
class NodeList {
constructor(data) {
this.data = data;
this.next = null;
}
}
//链表的结构
class LinkedList {
constructor() {
this.length = 0;
this.head = null;
}
append Node(){
}
……
}
链表的常用方法
1. appendNode(val):添加一个新节点
appendNode(val) {
// 先创建一个新节点,判断是否有头节点,如果没有则设置为头节点
// 如果有的话则找到最后一个节点并添加
let newNode = new NodeList(val)
if (!this.head) {
this.head = newNode;
} else {
let cur = this.head;
// 找到最后的节点
while (cur.next) {
cur = cur.next;
}
cur.next = newNode
}
this.length++;
}
2. insertNode(val,position)
向链表中插入节点,参数为节点的值以及插入位置。常规的插入是先找到待插入位置的pre节点,之后将cur节点的next指向pre的next,最后将pre的next的节点设置为curNode
// 指定位置插入节点
insertNode(val, position) {
// 1.先判断是否超出边界
if (position > this.length || position < 0) return false;
let newNode = new NodeList(val)
// position为0则表明将当前节点作为头节点
if (position === 0) {
newNode.next = this.head;
this.head = newNode
} else {
// 找到position的前一个节点
let previous = this.getNodeByposition(position - 1)
previous.next = newNode.next;
previous.next = newNode
}
this.length++
return true
}
3. removeAt(position)
移除指定位置的节点。常规的删除方法:找到待删除节点的preNode,最后将preNode的next指向preNode的next的next,也就是将curNode节点的上一个节点和下一个节点连在一起,没有指向的节点会在浏览器中自动被垃圾回收机制回收。
// 删除指定位置的节点,并返回被删除的元素的值
removeAt(position) {
// 先判断index是否在范围内
if (position >= this.length || position < 0) return false;
// 同样还要区分是否删除头节点
let current = this.head
let cur = this.getNodeByposition(position)
if (position === 0) {
this.head = current.next;
} else {
let previous = this.getNodeByposition(position - 1)
previous.next = previous.next.next;
}
this.length--;
return cur
}
4.getNodeBypostion(position)
通过指定位置获取当前的节点信息,返回的是节点类型的数据。
常规方法,将头指针从头开始指,将指针向后移动postion次
// 获取链表中对应索引的元素
getNodeByposition(position) {
// 先判断传入的position是否正常
if (position < 0 || position > this.length) {
return -1
}
let current = this.head;
while (position--) {
current = current.next
}
return current
}
5.getDataByPostion(position)
通过指定位置来获取节点的data。常规方法:从头开始遍历,当指针移动position次时,将node的data返回。
// 获取指定索引的值
getDataByPosition(position) {
if (position < 0 || position > this.length) {
return false
}
let current = this.head;
while (position--) {
current = current.next
}
return current.data;
}
6.removeElement(val)
给定值,将链表中的改数据的节点删除。常规方法:从头遍历,将每一个节点的值都与val进行比对,如果相同,就执行删除节点的函数。但这种方法只考虑到data不相同的情况,如果data相同则只会删除第一个
// 删除通过值删除链表元素
removeElement(val) {
let position = this.getPostionByData(val);
console.log(position);
let previous = this.getNodeByposition(position - 1);
console.log(previous);
let tempNode = previous.next;
if (position > -1) {
previous.next = previous.next.next
return tempNode
}
// 说明没有找到
else {
return false
}
}
7.clearNode()
清空当前的链表。常规方法:将链表的长度置零,头指针置空
// 清空链表
clearNode() {
this.head = null;
this.length = 0;
}
8.nodeToString()
将链表中每一个节点的数据都打印出来
nodeToString() {
let cur = this.head;
let listStr = '';
while (cur) {
listStr += cur.data + ' '
cur = cur.next
}
return listStr
}
9.updateNode(position,newVal)
将指定位置的节点数据替换。常规方法,先找到指定位置的节点,再将节点数据更新
// 将指定位置的值进行替换
updataNode(position, newVal) {
if (position < -1 || position > this.length) return false;
// 先找到指定位置的节点
let cur = this.getNodeByposition(position);
// 返回-1则说明没有找到
if (cur) {
cur.data = newVal
return true;
} else {
return false;
}
}
2.双向链表
双向链表就是在单链表的基础上给链表中的每个节点增加一个前驱节点,链表可以从头部遍历,也可以从尾部开始遍历
节点结构
相比于单向链表,双向链表中的每个节点都多了一个指向前驱节点的指针。也就是我们可以自由的访问到当前节点的上一个节点以及下一个节点。
// 节点类型
class NodeList {
constructor(val) {
// 包含数据节点,前驱节点以及下一个节点
this.data = val;
this.pre = null;
this.next = null;
}
}
链表结构
链表中多了一个指向尾部的指针,方便我们从尾部开始遍历
class DoubleLinked {
constructor() {
this.length = 0;
this.head = null;
this.tail = null
}
appendNode(){}
……
}
常用方法
常用的方法与之前的单链表类似,但是在细节上稍作变化,常用的比如有:appendNode、insertNode、removeNode、displayList……,使用的辅助方法有:getNodeByPosition、getPositionByData、getDataByPosition、getNodeByData。这里所说的辅助方法就是可以我们对经常使用的代码的封装,方便我们在其它方法中使用
1.appendNode(val)
向链表尾部插入元素,插入之前先判断所插入的是否为头节点。
appendNode(val) {
if (val == undefined) throw new Error('缺少参数');
let newNode = new NodeList(val)
// 判断链表是否有数据
if (this.head === null) {
this.head = newNode;
this.tail = newNode;
}
// 如果有的话,则向尾部添加
else {
this.tail.next = newNode;
newNode.pre = this.tail;
this.tail = newNode
}
this.length++;
return this.displayList()
}
2.getPositionByData(val)
通过传递的值来判断该元素在链表中的索引,比如知道该学生的姓名,但不知道学号,也就是索引。可以从头部和尾部同时查找,两个指针同时开始移动,如果链表中没有这个元素才会遍历链表。
getPositionByData(val) {
let head = this.head;
let tail = this.tail;
let index = 0;
while (head) {
if (head.data === val) return index;
head = head.next;
if (tail.data === val) return this.length - 1 - index;
tail = tail.pre;
index++;
}
// 否则说明没有找到,返回null
return null
}
3.getNodeByData(val)
通过传递的值来返回该值所在的节点,同样的以学生信息管理系统为例,我们只知道这个学生的名字,需要返回该学生的所有相关信息,此时就可以使用该方法,返回该节点,同时该节点中包含前驱指针和后继指针。
getNodeByData(val) {
// 头指针和尾指针
let headNode = this.head;
let tailNode = this.tail;
while (headNode) {
if (headNode.data == val) return headNode;
if (tailNode.data == val) return tailNode;
headNode = headNode.next;
tailNode = tailNode.pre;
}
// 没有找到就返回null
return null
}
4.getDataByPosition(position)
通过索引来返回该节点的值,同样的还是先找到该节点,之后再返回该节点的值
getDataByPosition(position) {
if (position >= this.length || position < 0) return false;
let middlePosition = Math.floor(position / 2);
let curNode = null;
if (position > middlePosition) {
curNode = this.tail;
let i = this.length - 1;
while (i > position) {
curNode = curNode.pre;
i--
}
} else {
curNode = this.head;
while (position--) {
curNode = curNode.next
}
}
return curNode.data
}
5.getNodeByPosition(position)
返回指定位置的节点,,比如返回某个班级中23号同学的信息。
getNodeByPosition(position) {
if (position >= this.length || position < 0) return false;
// 算法优化,如果大于二分之一,就从尾节点开始遍历
let middlePosition = Math.ceil(position / 2);
// 当前节点
let curNode = null;
if (position >= middlePosition) {
curNode = this.tail;
let i = this.length - 1
// 从后往前执行
while (i > position) {
curNode = curNode.pre;
i--;
}
} else {
curNode = this.head;
while (position--) {
curNode = curNode.next
}
}
return curNode
}
6. insertNode(position,val)
向指定位置插入元素,需要分三种情况进行判断。先判断它是否是尾部插入或者是头部插入,如果是头部插入的话还需要判断它是否作为头节点。最后一种情况则是大多数情况,也就是中间插入。此时只需要改变前驱节点以及后继节点的指向即可。
insertNode(position, val) {
if (arguments.length === 1) throw new Error('缺少参数');
if (position >= this.length || position < 0) return '索引越界'
// 判断是不是尾部插入
if (position === this.length - 1) {
// 尾部插入
return this.appendNode(val);
}
// 判断是不是头部插入
else if (position === 0) {
// 判断是否有头节点
if (this.head === null) {
this.appendNode(val)
} else {
// 有头节点的头部插入
let headNode = new NodeList(val);
headNode.next = this.head;
this.head.pre = headNode;
this.head = headNode;
}
}
// 中间插入,大多数情况
else {
let newNode = new NodeList(val);
let curNode = this.getNodeByPosition(position);
newNode.next = curNode;
newNode.pre = curNode.pre;
curNode.pre.next = newNode;
curNode.pre = newNode;
}
this.length++;
return true
}
7.remoNode(position)
删除指定位置的节点,返回值为被删除的节点。同样的还是需要判断是头部删除还是尾部删除,中间删除只需要改变前驱节点和后继节点的指向即可。
removeNodeByPosition(position) {
// 异常判断
if (position > this.length - 1 || position < 0) return null;
// 头部删除
let curNode = this.getNodeByPosition(position)
if (position = 0) {
this.head = this.head.next;
this.head.pre = null;
} else if (position === this.length - 1) {
this.tail = this.tail.pre;
this.tail.next = null;
}
// 移除中间元素
else {
let preNode = curNode.pre;
let nextNode = curNode.next;
preNode.next = nextNode;
nextNode.pre = preNode;
}
this.length--;
return curNode
}
8.displayList(join)
将链表中的所有数据以字符串的形式进行拼接并打印。可以接收参数拼接,参数默认为空格。
displayList(join = ' ') {
let curNode = this.head;
let str = '';
while (curNode) {
// 最后一个拼接的后面不用加连接的内容
curNode.next !== null ? str += curNode.data + join : str += curNode.data
curNode = curNode.next;
}
return str
}
调用
let linkList = new DoubleLinked()
linkList.appendNode('apple');
linkList.appendNode('banana');
linkList.appendNode('peach');
linkList.appendNode('7777')
linkList.getDataByPosition(2)
linkList.getPositionByData('apple')
console.log(linkList.displayList());