🌈链表
📌 链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。
🔔 🔔 回顾数组:
- ⭐️ 要存储多个元素,数组可能是最常用的数据结构
- ⭐️ 几乎每一种语言都有默认实现的数据结构
- 💥 但数组也有一些缺点:
- 💢 数组的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的(大多数编程语言的数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。
- 💢在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移
- 💢 那么,就可以选择另一种存储多个元素的方式,即链表
❓ 那么链表到底是什么?
🍄 链表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向NULL的指针。 可以把链表想象成火车:有一个火车头(head),火车头会连接一个车厢(节点node),车厢里有乘客(类似于数据),并且这个车厢会连接下一个车厢,依次类推。。。
⭐️链表的优点?
- 内存空间不是必须连续的,可以充分利用计算机内存,实现灵活的内存动态管理
- 链表不必在创建时就确定大小,并且大小可以无限的延伸下去
- 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多
💥 链表的缺点?
- 链表访问任何一个位置的元素时,都需要从头开始访问(无法跳过第一个元素访问任何一个元素)
- 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素
🌈🌈单向链表
📌 单向链表即链表的连接方向是单向的。
- 只能从头遍历到尾或者从尾遍历到头(一般从头到尾)
- 链表相连的过程是单向的
- 实现的原理是:上一个节点中有一个指向下一个的引用
🌈🌈🌈单向链表的封装
- 常见操作:
- append(): 向链表尾部添加一个新的项
- insert(position,element): 向链表的特定位置插入一个新的项
- get(position): 获取对应位置的元素
- indexOf(element): 返回元素在链表中的索引,如果链表中没有该元素则返回-1
- update(position,element): 修改某个位置的元素
- removeAt(position): 从列表的特定位置删除一项
- remove(element): 从列表中移动一项
- isEmpty(): 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
- size(): 返回链表包含的元素个数,与数组的length属性类似
- toString(): 如果链表项使用Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
📢 在做插入和删除操作时,需要对要操作的位置区分处理。删除方法一定要记得length-1…在封装单向链表和双向链表的代码中,两次我都忘记这一步,两次报错,一个坑掉进去两次。。。我可真是个人才
function LinkedList() {
// 链表中的节点元素
function Node(data) {
this.data = data
this.next = null
}
this.head = null
this.length =0
//append 添加元素 判断两种情况
LinkedList.prototype.append = function (data) {
const newNode = new Node(data)
if(this.length === 0){ //当链表中没有节点元素时,把head 指向 newNode
this.head = newNode
}else {
let current = this.head
while(current.next){ //当链表中存在节点元素时,遍历找出最后一个节点,并将其next指向newNode
current = current.next
}
current.next = newNode
}
this.length +=1 //将length值加1
}
//toString 方法
LinkedList.prototype.toString =function () {
let current = this.head
let linkedStr = ''
while(current){
linkedStr += current.data + ' '
current = current.next
}
return linkedStr
}
//insert 方法
LinkedList.prototype.insert = function (position,data) {
//首先对position 进行越界判断 当position 为负数或者 position 大于当前链表长度时, 返回false
if(position < 0 || position > this.length) return false
const newNode = new Node(data)
// 随后判断position 值为0 或者不为0的情况
if(position === 0) {// 当position值为0 时 即将元素插入第一个元素的位置 即将head指向当前元素,把当前元素的next指向原来的第一个元素
newNode.next = this.head
this.head = newNode
}else {//当position值不为0时,遍历找到这个位置的元素,以及前一个元素,以便修改前一个元素next指向,以及当前元素指向原来这个位置的元素
let index = 0;
let current = this.head
while(index++ < position -1){ //找到当前位置的前一个元素
current =current.next
}
newNode.next = current.next
current.next =newNode
}
this.length +=1
return true
}
//get 方法
LinkedList.prototype.get = function (position) {
//对position做越界判断处理 和insert不同的是,此时的position 不能等于当前的length
if(position < 0 || position >=this.length) return null
let current = this.head
let index = 0
while(index ++ < position){
current = current.next
}
return current.data
}
//indexOf 方法
LinkedList.prototype.indexOf = function (element) {
let index = 0;
let current = this.head
while(current){
if(current.data === element){
return index
}
index ++
current =current.next
}
return -1
}
//update 方法
LinkedList.prototype.update = function (position, element) {
// 对position做越界判断处理
if(position < 0 || position >= this.length) return false
let index = 0;
let current = this.head
while(index ++ < position){
current = current.next
}
current.data = element
return true
}
//removeAt 方法
LinkedList.prototype.removeAt = function (position) {
//对position做越界判断处理
if(position < 0 || position >= this.length) return null
//删除 两种处理方式
//-1.删除第一个元素 position=0
//-2.删除其他元素 position>0
let previous = this.head
let current = null
if(position === 0){ // 删除第一个链表节点 ===》将head指向第一个元素的next
this.head = this.head.next
}else { //删除其它元素 ===》 找到当前节点的前一个节点,将其指向当前元素的后一个节点
let index = 0;
while(index ++ < position-1){ //直接
previous = previous.next
}
//current 为当前节点的前一个节点
current = previous.next
previous.next =current.next
}
this.length -=1 //长度记着减1 !!!!
return current
}
//remove 方法
LinkedList.prototype.remove = function (element) {
//获取节点元素对应的索引位置
let position = this.indexOf(element)
//调用 removeAt 方法实现删除
this.removeAt(position)
}
//isEmpty 方法
LinkedList.prototype.isEmpty = function () {
return this.length === 0
}
//size 方法
LinkedList.prototype.size = function () {
return this.length
}
}
let linkedList = new LinkedList()
//测试append
linkedList.append('qwe')
linkedList.append('asd')
linkedList.append('zxc')
//测试toString
console.log(linkedList.toString());
//测试insert
linkedList.insert(0,'aaa')
linkedList.insert(2,'bbb')
linkedList.insert(5,'ccc')
console.log(linkedList.toString());
//测试get
console.log(linkedList.get(0));
console.log(linkedList.get(2));
console.log(linkedList.get(5));
//测试indexOf
console.log(linkedList.indexOf('asd'));
console.log(linkedList.indexOf('aaa'));
console.log(linkedList.indexOf('ccc'));
//测试update
linkedList.update(0,'qqq')
linkedList.update(2,'zzz')
linkedList.update(5,'uuu')
console.log(linkedList);
console.log(linkedList.toString());
//测试removeAt
linkedList.removeAt(0)
console.log(linkedList.toString());
linkedList.removeAt(2)
console.log(linkedList.toString());
linkedList.removeAt(3)
console.log(linkedList.toString());
//测试remove
linkedList.remove('qwe')
console.log(linkedList.toString());
linkedList.remove('zxc')
console.log(linkedList.toString());
//测试isEmpty/size
console.log(linkedList.isEmpty());
console.log(linkedList.size());
🌈🌈双向链表
🔔 🔔 回顾单向链表:
- 💥 单向链表只能从头遍历到尾或者从尾遍历到头。
- 💥 相连的过程是单向的。
- 💥 可以轻松的到达下一个节点,但是要回到前一个节点是很困难的。
- 💥 但是,在实际开发过程中,经常会遇到回到上一个节点的情况,因此,这里就需要双向链表来解决单向链表会遇到的问题
📌 双向链表即链表链接的过程是双向的
- 既可以从头遍历到尾,又可以从尾遍历到头
- 一个节点既有向前连接的引用,也有一个向后连接的引用
⭐️ 双向链表的特点?
- 可以使用一个 head 和 一个 tail 分别指向头部和尾部的节点
- 每个节点都由三部分组成:前一个节点指针(prev)/保存的元素(item)/后一个节点指针(next)
- 双向链表的第一个节点的prev是null
- 双向链表的最后一个节点的next是null
💥 双向链表的缺点?
- 每次插入或删除某个节点时,需要处理四个引用,而不是两个,也就是实现起来要困难一些
- 并且相比于单向链表,必然占用内存空间更大一些
🌈🌈🌈双向链表的封装
- 常见操作:
- append(): 向链表尾部添加一个新的项
- insert(position,element): 向链表的特定位置插入一个新的项
- get(position): 获取对应位置的元素
- indexOf(element): 返回元素在链表中的索引,如果链表中没有该元素则返回-1
- update(position,element): 修改某个位置的元素
- removeAt(position): 从列表的特定位置删除一项
- remove(element): 从列表中移动一项
- isEmpty(): 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
- size(): 返回链表包含的元素个数,数组的length属性类似
- toString(): 如果链表项使用Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
- forwardString(): 返回正向遍历的节点字符串形式 向前遍历
- backwardString(): 返回反向遍历的节点字符串形式 向后遍历
📢 单向链表在做插入和删除操作时只需要判断position是否为0进行不同的处理,而因为双向链表涉及头、尾、上一个节点下一个节点的指向问题,所以在做插入和删除操作时,需要对元素个数是否为0做判断,在元素个数不为0的情况下,需要判断position是否为0,是否等于元素的个数(新增),是否等于元素个数-1(删除),以及中间区间做不同的处理。
//封装双向链表
function DoublyLinkedList() {
//节点
function Node(data) {
this.data = data
this.prev = null
this.next = null
}
this.head = null
this.tail = null
this.length = 0
//1.append 方法
DoublyLinkedList.prototype.append = function (data) {
const newNode = new Node(data)
if(this.length ===0){ //如果当前链表长度为0,即还没有节点元素,那么当前这个元素就是第一个节点元素
//长度为0,添加第一个元素 即 head指向的元素,也是最后一个元素
this.head = newNode
this.tail = newNode
}else {
//新添加的元素的prev指向原来的最后一个元素即tail;原来的最后一个元素tail的next指向当前这个元素;当前元素变为最后一个节点元素
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
}
this.length +=1
}
//2.转成字符串方法
//2.1 toString 方法
DoublyLinkedList.prototype.toString = function () {
return this.backwardString()
}
//2.2 forwardString 方法
DoublyLinkedList.prototype.forwardString = function () {
//从后向前遍历
let current = this.tail
let resStr = ''
while(current){
resStr += current.data + ' '
current = current.prev
}
return resStr
}
//2.3 backwardString 方法
DoublyLinkedList.prototype.backwardString = function () {
//从前向后遍历
let current = this.head
let resStr = ''
while(current){
resStr += current.data + ' '
current = current.next
}
return resStr
}
//3.insert 方法
DoublyLinkedList.prototype.insert = function (position, data) {
//对position的值做越界处理
if(position < 0 || position > this.length) return false
let newNode = new Node(data)
/*几种状态判断处理 -1.length是否为0 | 在length不为0的情况下 -2.position是否为0 -3.position是否等于length -4 其他区间的值*/
//先判断当前length 是否为0
if(this.length === 0){
this.head = newNode
this.tail = newNode
}else {
//对position的几种值状态分开处理 0 | 0< & <length | length
if(position === 0){
// 将节点元素插入在第一个位置
newNode.next = this.head
this.head.prev = newNode
this.head = newNode
}else if(position === this.length){
// 将节点元素插入最后一个位置
newNode.prev = this.tail
this.tail.next = newNode
this.tail = newNode
}else {
//在节点中间插入 即要处理四个指向值
let current = this.head
let index = 0
while(index ++ < position){
current = current.next
}
newNode.next = current //插入元素的next 指向这个位置的原元素
newNode.prev = current.prev //插入元素的prev 指向这个位置的原元素的prev
current.prev.next = newNode //原位置的上一个节点元素的next指向 现插入元素
current.prev = newNode //原位置的当前元素的prev指向 现插入元素
}
}
this.length +=1
}
//4. get方法
DoublyLinkedList.prototype.get = function (position) {
//对position 做越界处理
if(position < 0 || position >=this.length) return null
//使用二分法 顺序 还是 倒序
const asc = position < this.length /2
let current = asc ? this.head : this.tail
let index = asc ? 0 : this.length -1
while(asc? index ++ < position : index -- > position ){
current = asc? current.next : current.prev
}
return current.data
}
//5.indexOf 方法
DoublyLinkedList.prototype.indexOf = function (data) {
let current = this.head
let index = 0
while(current){
if(current.data === data){
return index
}
current = current.next
index +=1
}
return -1
}
//6.update 方法
DoublyLinkedList.prototype.update = function (position, data) {
//对position做越界处理
if(position < 0 || position >= this.length) return false
//使用二分法
const asc = this.length /2 > position
let current = asc ? this.head : this.tail
let index = asc ? 0: this.length-1
while(asc ? index ++ < position : index -- > position){
current = asc ? current.next : current.prev
}
current.data = data
return true
}
//7.removeAt 方法
DoublyLinkedList.prototype.removeAt = function (position) {
//对position做越界处理
if(position < 0 || position >=this.length) return null
//分情况做处理
//-1 判断length 是否为1
//-2 当length 不为1 时 2-2 是否删除第一个 | 2-3 其他 | 2-4 是否删除最后一个
let current = this.head //默认给current 赋值第一个节点元素
if(this.length === 1){ //当链表中长度为1时,能走进来说明删除的就是第一个也是最后一个节点元素 因此这里只需要把head和tail的指向都置为null即可 没有任何引用指向的元素会被自动回收机制回收
this.head = null
this.tail = null
}else {
if( position === 0 ){ //当链表长度不为1 且 删除第一个节点元素时,即把head指向第二个节点元素,且把第二个节点元素的prev指向 置为null
//即删除第一个节点元素
this.head.next.prev = null
this.head = this.head.next
}else if( position === this.length-1 ){ //当链表长度不为1 且 删除最后一个节点元素时,即把tail指向最后一个节点的前一个节点,且把前一个节点的next指向 置为null
current = this.tail
this.tail.prev.next = null
this.tail = this.tail.prev
}else { // 当链表长度不为1 且 删除的是 中间的某一个节点元素
let index = 0
while(index++ < position){
current = current.next
}
//把当前元素的上一个节点的next 指向 当前元素的下一个节点
//把当前元素的下一个节点的prev 指向 当前元素的上一个节点
current.prev.next = current.next
current.next.prev = current.prev
}
}
// 切记 !!!!!!!!!!!!!!!!length -1 ----一个坑栽了两次
this.length -=1
return current.data
}
// 8.remove 方法
DoublyLinkedList.prototype.remove = function (data) {
const index = this.indexOf(data)
this.removeAt(index)
}
// 9.isEmpty 方法
DoublyLinkedList.prototype.isEmpty = function () {
return this.length === 0
}
// 10.size 方法
DoublyLinkedList.prototype.size = function () {
return this.length
}
//11.获取头 -- 获取第一个节点元素
DoublyLinkedList.prototype.getHead = function () {
return this.head.data
}
//12.获取尾 -- 获取最后一个节点元素
DoublyLinkedList.prototype.getTail = function () {
return this.tail.data
}
}
//测试
let doublyLinkedList = new DoublyLinkedList()
//测试append
doublyLinkedList.append('qwe')
doublyLinkedList.append('asd')
doublyLinkedList.append('zxc')
//测试 转字符串
// alert(doublyLinkedList)
console.log(doublyLinkedList);
console.log(doublyLinkedList.toString());
console.log(doublyLinkedList.backwardString());
console.log(doublyLinkedList.forwardString());
//测试insert 方法
doublyLinkedList.insert(0,'aaa')
doublyLinkedList.insert(4,'bbb')
doublyLinkedList.insert(3,'ccc')
console.log(doublyLinkedList.toString());
//测试get
console.log(doublyLinkedList.get(0));
console.log(doublyLinkedList.get(2));
console.log(doublyLinkedList.get(6));
console.log(doublyLinkedList.get(5));
//测试indexOf
console.log(doublyLinkedList.indexOf('aaa'));
console.log(doublyLinkedList.indexOf('ccc'));
console.log(doublyLinkedList.indexOf('bbb'));
//测试update 方法
doublyLinkedList.update(0, 'kkk')
doublyLinkedList.update(5, 'hhh')
doublyLinkedList.update(3, 'ooo')
console.log(doublyLinkedList.toString());
//测试removeAt
// console.log(doublyLinkedList.removeAt(0));
// console.log(doublyLinkedList.removeAt(4));
// console.log(doublyLinkedList.removeAt(2));
// console.log(doublyLinkedList.toString());
// 测试remove
doublyLinkedList.remove('kkk')
doublyLinkedList.remove('lll')
doublyLinkedList.remove('ooo')
console.log(doublyLinkedList.toString());
console.log(doublyLinkedList.isEmpty());
console.log(doublyLinkedList.size());
console.log(doublyLinkedList.getHead());
console.log(doublyLinkedList.getTail());