JavaScript数据结构之链表

🌈链表

📌 链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同。

🔔 🔔 回顾数组:

  • ⭐️ 要存储多个元素,数组可能是最常用的数据结构
  • ⭐️ 几乎每一种语言都有默认实现的数据结构
  • 💥 但数组也有一些缺点:
    • 💢 数组的创建通常需要申请一段连续的内存空间(一整块内存),并且大小是固定的(大多数编程语言的数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。
    • 💢在数组的开头或中间位置插入数据的成本很高,需要进行大量元素的位移
    • 💢 那么,就可以选择另一种存储多个元素的方式,即链表

那么链表到底是什么?

🍄 链表是由结点构成,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());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值