基于javaScript的数据结构之链表

链表

  • 基于栈和队列解构,我们可以发现,这两种结构虽然读取数据时是非常优秀的。但是它在数组的起点或中间进行插入或删除数据时,成本很高。虽然在js中,我们(开发者)不像java那样,需要手动对数组进行扩容操作,但是这种操作在js底层还是要做的。那么,对于这种新增或删除的操作,有没有另一种高效率的存储结构呢?那肯定是有的,比如,本次分享的链表这种数据结构。链表的结构,有很多种,比如:基础链表(单向链表)、双向链表、循环列表、排序链表等。故名思意,不同结构的链表,可以更好的实现不同的功能。下图,是针对单向链表的示意图:
    单向链表示意图
  • 下面,我使用js依次将其进行实现(单向链表、双向、循环)。

单向链表

代码实现如下:

// 链表
function LikedList(cal = function (a,b) { return a == b }) {
    this.count = 0;
    this.head = null;
    this.equalsFn = cal;
    // 向列表末尾追加元素
    LikedList.prototype.push = function (data) {
        let node = new Node(data);
        if (!this.head) {
            this.head = node;
        } else {
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            current.next = node;
        }
        this.count++;
    }
    // 通过索引插入元素
    LikedList.prototype.insert = function (data, index) {
        if (index >= 0 && index <= this.count) {
            let node = new Node(data);
            if (index == 0) {
                node.next = this.head;
                this.head = node;
            } else {
                let previous = this.getElementFromIndex(index - 1);
                // 减少循环的次数
                // let current = this.getElementFromIndex(index);
                let current = previous.next;
                previous.next = node;
                node.next = current;
            }
            this.count++
        }
        return false;
    }
    // 通过索引移除元素
    LikedList.prototype.removeFromIndex = function (index) {
        if (this.isEmpty()) return undefined;
        if (index >= 0 && index < this.count) {
            let current = this.head;
            if (index == 0) {
                this.head = current.next;
            } else {
                let previous = this.getElementFromIndex(index - 1);
                current = previous.next;
                previous.next = current.next;
            }
            this.count--;
            return current.data;
        }
        return undefined;
    }
    // 通过元素指定key(id),删除. 此时cal回调函数必填
    LikedList.prototype.remove = function (ele) {
        if (this.isEmpty() || !ele ) return undefined;
        let current = this.head;
        let currentIndex = this.indexOf(ele);
        if(currentIndex < 0) return undefined;
        while(current){
            if(this.equalsFn(current.data, ele)){
                let previous = this.getElementFromIndex(currentIndex - 1);
                previous.next = current.next;
                this.count--;
                return current.data;
            }else{
                current = current.next
            }
        }
    }
    // 通过元素,返回对应的索引
    LikedList.prototype.indexOf = function (ele) {
        if (this.isEmpty()) return -1;
        if (!ele) return -1;
        let current = this.head;
        for(let i=0;i<this.count && current;i++){
            if(this.equalsFn(ele, current.data)){
                return i
            }else{
                current = current.next;
            }
        }
        return -1
    }
    // 通过指定的索引获取元素
    LikedList.prototype.getElementFromIndex = function (index) {
        if (index >= 0 && index <= this.count) {
            let node = this.head;
            for (let i = 0; i < index && node != null; i++) {
                node = node.next;
            }
            return node;
        }
        return undefined;
    }
    // 判断是否尾空链表
    LikedList.prototype.isEmpty = function () {
        return !this.count;
    }
    // 获取链表的大小
    LikedList.prototype.size = function () {
        return this.count;
    }
    // 获取链表的第一个元素
    LikedList.prototype.getHead = function () {
        return this.head;
    }
    // 清空链表
    LikedList.prototype.clear = function () {
        this.head = null;
        this.count = 0;
    }
    // 链表toString方法
    LikedList.prototype.toString = function () {
        if (!this.head) return "";
        let str = "";
        let current = this.head;
        while (current) {
            let objStr = JSON.stringify(current.data);
            str += objStr += ",";
            current = current.next;
        }
        return str.slice(0, str.length - 1);
    }

    function Node(data = null) {
        this.data = data;
        this.next = null;
    }
}

let list = new LikedList(function (a, b) {
    return a.id == b.id
});
list.push({ id: 1 }) // 0
list.push({ id: 2 }) // 2
list.insert({ id: 3 }, 1) // 1
let res = list.remove({ id: 3 });
console.log(res) // Node,
console.log(list)

对于LikedList类,实现的功能:*push、insert、removeFromIndex、remove、indexOf、getElementFromIndex、*isEmpty、size、getHead、clear、toString。另外,在创建单向链表实例是,可以传入一个回调(cal,虽然有默认值,但仅用于简单之判断),用于进行复杂数据的判断,因为在进行元素删除(remove)、索引查找(indexOf)时,如果data是简单值,貌似并没有太大问题(cal = function(a,b)=>a==b),但如果data是复杂值(Object),那么,在创建单向链表实例,就需要传入自定义回调,用于唯一值的判断(这里是id)。另外在LikedList类中,还封装了一个Node的类,它用于创建每一个传入的数据,并保存next(指向)。

双向链表

写完上面的单向链表,再写双向链表,就容易了一些,所谓的双向链表,就在在单向链表的基础上,把每个node的节点上,添加一个pre属性,用来记录与它相关的上一个节点的数据,代码实现如下:

// 双向链表
function DoubleLikedList(cal = (a, b) => a == b) {
  this.head = null;
  this.tail = null;
  this.isEqual = cal;
  this.count = 0;
  // 在索引index之前,插入数据
  // index != undefined时,从指定索引处追加元素
  // index == undefined时,默认从链表尾追加元素
  DoubleLikedList.prototype.insert = function (data, index) {
    if (index == undefined) {
      this.insert(data, this.count);
    }
    if (index >= 0 && index <= this.count) {
      let node = new Node(data);
      let current = this.head;
      if (index == 0) {
        // 表头插入
        if (this.count == 0) {
          this.head = this.tail = node
        } else {
          this.head = node;
          node.next = current;
          current.pre = node;
        }
      } else if (index == this.count) {
        // 表尾插入
        node.pre = current = this.tail;
        current.next = this.tail = node;
      } else {
        // 中间任一位置插入
        let previousNode = this.getElementFromIndex(index - 1);
        current = previousNode.next;
        previousNode.next = node;
        node.pre = previousNode;
        node.next = current;
        current.pre = node;
      }
      this.count++;
      return true;
    }
    return false;
  }
  // 通过索引删除
  DoubleLikedList.prototype.removeAt = function (index) {
    if (index >= 0 && index < this.count) {
      let current = this.head;
      if (index == 0) {
        this.head = current.next;
        if (this.count == 1) {
          this.tail = null
        } else {
          this.head.pre = null;
        }
      } else if (index == this.count - 1) {
        current = this.tail;
        this.tail = current.pre;
        this.tail.next = null;
      } else {
        current = this.getElementFromIndex(index);
        let previous = current.pre;
        previous.next = current.next;
        current.next.pre = previous;
      }
      this.count--;
      return current.data;
    }
    return undefined
  }
  // 获取元素->索引
  DoubleLikedList.prototype.indexOf = function (data) {
    if (this.isEmpty() || !data) return -1;
    // 这里不好做优化,
    // 如果做优化的话,只能先从中间前端,如果没找到,
    // 再从中间往后端找,最后返回值。
    // 照着这个思路,此方法优化节约的时间,所在范围只能在 
    // (1/4, 1/2) * this.count 处,才有效果
    // 类似于正常逻辑中的 (0, 1/2) * this.count 处的效果
    // 所以,`优化逻辑的代码`并不是真正意义上的优化,
    // `优化逻辑的代码`,只是我的一个思考,暂放于此
    // 正常逻辑
    let cnt = 0
    let current = this.head;
    let index = 0;
    while (current) {
      cnt++;
      if (this.isEqual(data, current.data)) {
        console.log(cnt)
        return index;
      }
      index++;
      current = current.next;
    }
    // `优化逻辑`
    // let midIndex = Math.floor(this.count / 2);
    // let current = this.getElementFromIndex(midIndex);
    // while (current && midIndex > 0) {
    //   cnt++;
    //   if (this.isEqual(data, current.data)) {
    //     console.log(cnt)
    //     return midIndex;
    //   }
    //   midIndex--;
    //   current = current.pre;
    // }
    // midIndex = Math.floor(this.count / 2);
    // current = this.getElementFromIndex(midIndex);
    // while (current && midIndex < this.count) {
    //   cnt++;
    //   if (this.isEqual(data, current.data)) {
    //     console.log(cnt)
    //     return midIndex;
    //   }
    //   midIndex++;
    //   current = current.next;
    // }
    return -1;
  }
  // 获取索引->元素
  DoubleLikedList.prototype.getElementFromIndex = function (index) {
    if (index >= 0 && index <= this.count) {
      // 方案一:从头一次索引查找,性能普通
      // let current = this.head;
      // for (let i = 0; i < index && current != null; i++) {
      //   cnt++;
      //   current = current.next;
      // }

      // 方案二:判断index是否为count的一半,性能提升(约提升一半,在大数据量下较为明显)
      // 为何此处举节约一半的性能,而不是1/3、1/4或者其它的呢
      // 因为默认情况下,双向链表可以立即取到的值,只有 this.head 和 this.tail
      // 所以在循环过程中,只能(最容易)基于这两个值来进行取值(current)
      let halfCount = Math.floor(this.count / 2);
      let current;
      if (index >= halfCount) {
        current = this.tail;
        for (let i = this.count - 1; i > index && current != null; i--) {
          current = current.pre
        }
      } else {
        current = this.head;
        for (let i = 0; i < index && current != null; i++) {
          current = current.next;
        }
      }
      return current;
    }
    return null;
  }
  // 为空判断
  DoubleLikedList.prototype.isEmpty = function () {
    return !this.count
  }
  function Node(data) {
    this.data = data;
    this.next = null;
    this.pre = null
  }
}
let obj = new DoubleLikedList((a, b) => {
  return a.id == b.id;
});
obj.insert({ id: 1 });
obj.insert({ id: 2 });
obj.insert({ id: 3 });
obj.insert({ id: 4 });
obj.insert({ id: 5 });
obj.insert({ id: 6 });
obj.insert({ id: 7 });
obj.insert({ id: 8 });
obj.insert({ id: 9 });
obj.insert({ id: 10 });
let index = obj.indexOf({ id: 5 })
console.log(obj)

以上就是双向链表的实现,代码可独立运行,没有做继承依赖。在双向链表的索引查找元素方法中,做了一个循环判断的优化,用来在略大数据量下的优化。具体功能说明及实现,可见注释。

循环链表

熟悉了链表的基本结构之后,那么到循环链表这,就不攻自破了,循环链表,指的就是,在单向链表或双向链表的head或tail的指向不为null,比如基于双向链表的循环链表,它的head的pre,指向了tail;而tail.next指向了head,下面用代码实现基于双向链表的循环链表:

// 循环链表(基于双向链表的循环)
function CircularLikedList(cal = (a, b) => a == b) {
  this.count = 0;
  this.head = null;
  this.tail = null;
  this.isEqual = cal;
  // 插入元素
  CircularLikedList.prototype.insert = function (data, index) {
    if (index == undefined) {
      this.insert(data, this.count);
    }
    if (index >= 0 && index <= this.count) {
      let current = this.head;
      let node = new Node(data);
      if (index == 0) {
        // 新头
        this.head = node;
        if (this.count == index) {
          // 新头 && 新尾
          this.tail = node;
          node.pre = node.next = node;
        } else {
          // 换新头
          node.next = current;
          node.pre = this.tail;
          current.pre = node;
          this.tail.next = node;
        }
      } else if (index == this.count) {
        // 新尾(不存在新头 && 新尾,此种情况在上面已经判断),this.count > 0
        current = this.tail;
        this.head.pre = node;
        this.tail = node;
        node.pre = current;
        node.next = this.head;
        current.next = node
      } else {
        // 不存在新头/新尾,且this.count > 0
        let previous = this.getElementFromIndex(index - 1);
        current = previous.next;
        previous.next = node;
        node.pre = previous;
        node.next = current;
        current.pre = node;
      }
      this.count++;
      return true
    }
    return false;
  }
  // 索引->元素
  CircularLikedList.prototype.getElementFromIndex = function (index) {
    if (index == undefined) return -1;
    if (index >= 0 && index < this.count) {
      let midIndex = Math.floor(this.count / 2);
      let current, curIndex;
      if (index > midIndex) {
        // 从后面索引
        current = this.tail;
        curIndex = this.count;
        while (current) {
          if (--curIndex == index) {
            return current
          }
          current = current.pre;
        }
        return undefined
      } else {
        // 从前面索引
        current = this.head;
        curIndex = 0;
        while (current) {
          if (curIndex++ == index) {
            return current
          }
          current = current.next;
        }
        return undefined
      }
    }
    return undefined
  }
  // 删除
  CircularLikedList.prototype.remove = function (data) {
    if (data == undefined) return undefined;
    if (this.isEmpty()) return undefined;
    let current = this.tail;
    while (current) {
      if (this.isEqual(data, current.data)) {
        current.pre.next = current.next;
        current.next.pre = current.pre
        this.count--;
        return data
      }
      current = current.next;
    }
    return undefined;
  }
  // 为空判断
  CircularLikedList.prototype.isEmpty = function () {
    return !this.count
  }
  function Node(data) {
    this.data = data;
    this.pre = null;
    this.next = null;
  }
}
let circularList = new CircularLikedList((a, b) => a.id == b.id);
circularList.insert({ id: 1 })
circularList.insert({ id: 2 })
circularList.insert({ id: 3 })
circularList.insert({ id: 4 }, 1) // 1 4 2 3
let rm = circularList.remove({id:4})
console.log(rm)
console.log(circularList)

对于循环链表的实现,这里没有特殊的感悟,主要还是看具体的使用场景,个人任务,循环链表使用的场景并不多。
以上便是,基于javaScript来实现的链表接口(‘单向’、双向、循环)。另外还有一个链表结构,有序链表(可以模拟栈结构(tail),也可以模拟队列(head)),有序链表模拟栈结构或队列,有个优势,就是,在大数据量下,数组会有些乏力(需要开辟连续内存空间)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值