• JS数据结构第二篇---链表


    一、什么是链表 

    链表是一种链式存储的线性表,是由一组节点组成的集合,每一个节点都存储了下一个节点的地址;指向另一个节点的引用叫链;和数组中的元素内存地址是连续的相比,链表中的所有元素的内存地址不一定是连续的。结构模拟如图:

    一般来说,说到链表,就要提下数组,一般链表都是和数组进行对比。

    在很多编程语言中,数组的长度时固定的,所以数组中的增加和删除比较麻烦,需要频繁的移动数组中的其他元素。

    然而,JavaScript中的数组并不存在上述问题,JS中的数组相对其他语言使用上更方便,因为JS中的数组本质是一个类似数组的对象,这就使得JS的数组虽然使用更方便,但比其他语言(C++、Java、C#)的数组效率要低。

    所以,在实际应用中如果发现数组很慢,就可以考虑使用链表来替代它。除了对数据的随机访问,链表几乎可以用在任何可以使用一维数组的情况中。如果需要随机访问,数组仍然是更好的选择。

    二、链表的设计

    为了对链表更好的使用,我们设计了类LinkedList, 对链表中节点的增删改查方法进行了封装。结构如图:

    其中size和head为LinkedList构造函数私有属性,size记录链表中有多少个节点,head指向链表的头结点。

    根据需要对外暴露了以下方法(可以根据需要自定义其他方法):

     单向LinkedList完整设计代码:

    /**
     * 自定义链表:对外公开的方法有
     * append(element) 在链表最后追加节点
     * insert(index, element) 根据索引index, 在索引位置插入节点
     * remove(element)  删除节点
     * removeAt(index)  删除指定索引节点
     * removeAll(element) 删除所有匹配的节点
     * set(index, element) 根据索引,修改对应索引的节点值
     * get(index)  根据索引获取节点信息
     * indexOf(element) 获取某个节点的索引位置
     * clear()  清空所有节点
     * length()   返回节点长度
     * print() 打印所有节点信息
     * toString() 打印所有节点信息,同print
     * */
    const LinkedList = function(){
        let head = null;
        let size = 0;   //记录链表元素个数
    
        //Node模型
        function LinkNode(element, next){
            this.element = element;
            this.next = next;
        }
    
        //元素越界检查, 越界抛出异常
        function outOfBounds(index){
            if (index < 0 || index >= size){
                throw("抱歉,目标位置不存在!");
            }
        }
    
        //根据索引,获取目标对象
        function node(index){
            outOfBounds(index);
    
            let obj = head;
            for (let i = 0; i < index; i++){
                obj = obj.next;
            }
    
            return obj;
        }
    
        //新增一个元素
         function append(element){
            if (size == 0){
                head = new LinkNode(element, null);
            }
            else{
                let obj = node(size-1);
                obj.next = new LinkNode(element, null);
            }
             size++;
        }
    
        //插入一个元素
         function insert(index, element){
            if (index == 0){
                head = new LinkNode(element, head);
            }
            else{
                let obj = node(index-1);
                obj.next = new LinkNode(element, obj.next);
            }
             size++;
        }
    
        //修改元素
        function set(index, element){
            let obj = node(index);
            obj.element = element;
        }
    
        //根据值移除节点元素
        function remove(element){
            if (size < 1) return null;
    
            if (head.element == element){
                head = head.next;
                size--;
                return element;
            }
            else{
                let temp = head;
                while(temp.next){
                    if (temp.next.element == element){
                        temp.next = temp.next.next;
                        size--;
                        return element;
                    }
                    else{
                        temp = temp.next;
                    }
                }
            }
            return null;
        }
    
        //根据索引移除节点
         function removeAt(index){
             outOfBounds(index);
             let element = null;
    
             if (index == 0){
                 element = head.element;
                 head = head.next;
             }
             else{
                 let prev = node(index-1);
                 element = prev.next.element;
                 prev.next = prev.next.next;
             }
             size--;
            return element;
        }
    
        //移除链表里面的所有匹配值element的元素
         function removeAll(element){
    
            let virHead = new LinkNode(null, head); //创建一个虚拟头结点,head为次节点
             let tempNode = virHead, ele = null;
    
             while(tempNode.next){
                 if (tempNode.next.element == element){
                     tempNode.next = tempNode.next.next;
                     size--;
                     ele = element;
                 }
                 else{
                    tempNode = tempNode.next;
                 }
             }
    
             //重新赋值
             head = virHead.next;
    
            return ele;
        }
    
        //获取某个元素
        function get(index){
            return node(index).element;
        }
    
        //获取元素索引
        function indexOf(element){
            let obj = head, index = -1;
    
            for (let i = 0; i < size; i++){
                if (obj.element == element){
                    index = i;
                    break;
                }
                obj = obj.next;
            }
            return index;
        }
    
        //清除所有元素
        function clear(){
            head = null;
            size = 0;
        }
    
        //属性转字符串
        function getObjString(obj){
    
            let str = "";
    
            if (obj instanceof Array){
                str += "[";
                for (let i = 0; i < obj.length; i++){
                    str += getObjString(obj[i]);
                }
                str = str.substring(0, str.length - 2);
                str += "], "
            }
            else if (obj instanceof Object){
                str += "{";
                for (var key in obj){
                    let item = obj[key];
                    str += """ + key + "": " + getObjString(item);
                }
                str = str.substring(0, str.length-2);
                str += "}, "
            }
            else if (typeof obj == "string"){
                str += """ + obj + """ + ", ";
            }
            else{
                str += obj + ", ";
            }
    
            return str;
        }
        function toString(){
            let str = "", obj = head;
            for (let i = 0; i < size; i++){
                str += getObjString(obj.element);
                obj = obj.next;
            }
            if (str.length > 0) str = str.substring(0, str.length -2);
            return str;
        }
        //打印所有元素
        function print(){
            console.log(this.toString())
        }
    
        //对外公开方法
        this.append = append;
        this.insert = insert;
        this.remove = remove;
        this.removeAt = removeAt;
        this.removeAll = removeAll;
        this.set = set;
        this.get = get;
        this.indexOf = indexOf;
        this.length = function(){
            return size;
        }
        this.clear = clear;
        this.print = print;
        this.toString = toString;
    }
    
    
    ////测试
    // let obj = new LinkedList();
    // let obj1 = { title: "全明星比赛", stores: [{name: "张飞vs岳飞", store: "2:3"}, { name: "关羽vs秦琼", store: "5:5"}]};
    //
    // obj.append(99);
    // obj.append("hello")
    // obj.append(true)
    // obj.insert(3, obj1);
    // obj.insert(0, [12, false, "Good", 81]);
    // obj.print();
    // console.log("obj1.index: ", obj.indexOf(obj1));
    // obj.remove(0);
    // obj.removeAll(obj1);
    // obj.print();
    
    ////测试2
    console.log("
    
    ......test2.....")
    var obj2 = new LinkedList();
    obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
    obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
    obj2.print();
    obj2.removeAll(8); //删除所有8
    obj2.print();
    View Code

    另外,可以在LinkedList中增加一个虚拟节点,即在头结点之前增加一个节点,一直保留,结构如图:

    这里代码就不提供了,在上一份链表代码中的removeAll(删除链表中指定值的所有节点)方法中有用到虚拟头结点, 下面的练习题中也有应用到虚拟头结点,应用场景还是蛮多的。

    三、链表练习题

    推荐一个神奇的网站,可以以动画的方式演示各种数据结构增删改查变化,先来张展示链表的增删效果图看看:

    网址:https://visualgo.net/zh

    接下来做几个链表的练习题,题目来自力扣,可以先自己先做一下,看看自己得分,再对比下官方提供的代码demo

    3.1 删除排序链表中的重复元素_第83题

    参考demo:

    /**
     * 给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
     示例 1:
     输入: 1->1->2
     输出: 1->2
    
     示例 2:
     输入: 1->1->2->3->3
     输出: 1->2->3
    
     力扣得分:
     执行用时 :108 ms, 在所有 JavaScript 提交中击败77.12%的用户
     内存消耗 :37.4 MB, 在所有 JavaScript 提交中击败了5.03%的用户
     */
    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    /**
     * @param {ListNode} head
     * @return {ListNode}
     */
    var deleteDuplicates = function(head) {
    
        let virHead = new ListNode(0); //增加一个虚拟节点
        virHead.next = head;
        let temp = virHead, obj = {};
    
        while(temp.next){
            if (obj[temp.next.val]){ //表示为重复节点,删除这个节点
                temp.next = temp.next.next;
            }
            else{ //
                obj[temp.next.val] = 1;
                temp = temp.next;
            }
        }
        return virHead.next;
    }
    
    //测试
    var obj = new ListNode(1);
    obj.next = new ListNode(2);
    obj.next.next = new ListNode(1);
    obj.next.next.next = new ListNode(3);
    obj.next.next.next.next = new ListNode(1);
    obj.next.next.next.next.next = new ListNode(2);
    obj.next.next.next.next.next.next = new ListNode(3);
    console.log(obj);
    console.log(".>>>>>>删除重复节点:")
    console.log(deleteDuplicates(obj));
    View Code

    3.2 判断是否环形链表_第141题

    参考demo:

    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    
    /**
     * @param {ListNode} head
     * @return {boolean}
     */
    var hasCycle = function(head) {
        //快慢指针,快指针每次走两步,慢指针每次走一步
        let obj1 = head, obj2 = head; //obj1快指针,obj2为慢指针
    
        while(obj2){
          obj2 = obj2.next;
    
          if (obj1){
              obj1 = obj1.next;
          }
    
          if (obj1){
              obj1 = obj1.next;
          }
    
          if (obj2 == obj1 && obj1) return true;
        }
        return false;
    };
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    //测试
    console.log(">>>>>>环形链表》》测试》》")
    let node1 = new ListNode(1);
    let node2 = new ListNode(2);
    let node3 = new ListNode(3);
    let node4 = new ListNode(4);
    
    node1.next = node2;
    node2.next = node3;
    node3.next = node4;
    node4.next = node2;
    
    let res = hasCycle(node1);
    console.log("res: ", res);
    View Code

    3.3 移除链表中给定值的所有元素_第203题

    参考demo1:

    /**
     删除链表中等于给定值 val 的所有节点。
     示例:
     输入: 1->2->6->3->4->5->6, val = 6
     输出: 1->2->3->4->5
    
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    /**
     * 在力扣中得分:耗时160ms, 打败Javascript中17.87%; 内存消耗37.5M, 打败JavaScript中24.79% , 更优化的写法是?
     * @param {ListNode} head
     * @param {number} val
     * @return {ListNode}
     */
    var removeElements = function(head, val) {
        let newHead = null, curNode = null;
        while(head){
            if (head.val != val){
                if (curNode){
                    curNode.next = new ListNode(head.val);
                    curNode = curNode.next;
                }
                else{
                    curNode = new ListNode(head.val);
                    newHead = curNode;
                }
            }
            head = head.next;
        }
        return newHead;
    }
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    
    //测试
    console.log(">>>>移除链表元素测试》》》")
    var node = new ListNode(1);
    node.next = new ListNode(2);
    // node.next.next = new ListNode(5);
    // node.next.next.next = new ListNode(4);
    // node.next.next.next.next = new ListNode(6);
    // node.next.next.next.next.next = new ListNode(8);
    // node.next.next.next.next.next.next = new ListNode(4);
    
    // var newNode = removeElements(node, 6);
    // console.log(newNode);
    
    var newNode = removeElements(node, 2);
    console.log(newNode);
    View Code

    参考demo2:

    /**
     删除链表中等于给定值 val 的所有节点。
     示例:
     输入: 1->2->6->3->4->5->6, val = 6
     输出: 1->2->3->4->5
    
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    /** 第二种写法
     * 在力扣中得分:耗时112ms, 打败Javascript中90.28%; 内存消耗37.5M, 打败JavaScript中24.79%
     * @param {ListNode} head
     * @param {number} val
     * @return {ListNode}
     */
    var removeElements = function(head, val) {
        if (!head) return head;
    
        let newHead = new ListNode(-1);
        newHead.next = head; //把head作为newHead的下一个
        let tmpNode = newHead;
    
        while(tmpNode.next){
            if (tmpNode.next.val == val){
                tmpNode.next = tmpNode.next.next;
            }
            else{
                tmpNode = tmpNode.next;
            }
        }
        return newHead.next; //返回newHead的下一个,就是我们想要的结果
    }
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    
    //测试
    console.log(">>>>移除链表元素测试》》》")
    var node = new ListNode(1);
    node.next = new ListNode(2);
    // node.next.next = new ListNode(5);
    // node.next.next.next = new ListNode(4);
    // node.next.next.next.next = new ListNode(6);
    // node.next.next.next.next.next = new ListNode(8);
    // node.next.next.next.next.next.next = new ListNode(4);
    
    // var newNode = removeElements(node, 6);
    // console.log(newNode);
    
    var newNode = removeElements(node, 2);
    console.log(newNode);
    View Code

    3.4 反转链表_第206题

    参考demo1_迭代方式:

    /*
     反转一个单链表。使用迭代方式实现
     示例:
     输入: 1->2->3->4->5->NULL
     输出: 5->4->3->2->1->NULL
    
     力扣中测试执行用时 : 76 ms, 在所有 JavaScript 提交中击败了97.74%的用户
     内存消耗 :36 MB, 在所有 JavaScript 提交中击败了6.92%的用户
     * */
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    /**
     * @param {ListNode} head
     * @return {ListNode}
     */
    var reverseList = function(head) {
        let newHead = null;
        while(head){
            let tmpNode= newHead;
            newHead = new ListNode(head.val);
            newHead.next = tmpNode;
            head = head.next;
        }
        return newHead;
    }
    
    
    ////测试
    var node = new ListNode(9);
    node.next = new ListNode(99);
    node.next.next = new ListNode(999);
    node.next.next.next = new ListNode(33);
    
    console.log("原链表:", node);
    console.log(".....反转....")
    console.log(reverseList(node))
    View Code

    参考demo2_递归方式:

    /*
     反转一个单链表。 使用递归方式实现
     示例:
     输入: 1->2->3->4->5->NULL
     输出: 5->4->3->2->1->NULL
    
     力扣测试得分:
     执行用时 :80 ms, 在所有 JavaScript 提交中击败了95.56%的用户
     内存消耗 :36.3 MB, 在所有 JavaScript 提交中击败了5.03%的用户
    * */
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    /**
     * @param {ListNode} head
     * @return {ListNode}
     */
    var reverseList = function(head) {
        return getNewNode(head).first;
    }
    
    /**
     * 递归,好绕啊:
     * 推演:加入2->3->4->5 递归:
     * @param node
     */
    function getNewNode(node){
    
        if (!node) return {first: null, cur: null };
    
        var cur = new ListNode(node.val);
    
        ////一直递归递归,拿到原链表最后一个元素开始返回
        var res = getNewNode(node.next);
    
        if (res.first) {
            res.cur.next = cur; //设置
    
            return {
                first: res.first, //反转链表的第一个元素
                cur: cur
            }
        }
    
        console.log("666_node.val: ", node.val);
        /**
         * 原链表最后一个元素会执行到这里,最后一个元素作为反转链表的第一个元素返回
         */
    
        return {
            first: cur, //反转链表的第一个元素
            cur: cur    //每次递归返回的一个元素
        };
    }
    
    //测试
    var node = new ListNode(2);
    node.next = new ListNode(3);
    node.next.next = new ListNode(4);
    node.next.next.next = new ListNode(5);
    console.log("
    
    *****原链表****")
    console.log(node);
    console.log("......反转.....")
    console.log(reverseList(node));
    View Code

    3.5 查找链表的中间结点_第876题

    参考代码demo1_迭代方式:

    /**
     * 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
     如果有两个中间结点,则返回第二个中间结点。
    
     示例 1:
     输入:[1,2,3,4,5]
     输出:此列表中的结点 3 (序列化形式:[3,4,5])
     返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
     注意,我们返回了一个 ListNode 类型的对象 ans,这样:
     ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
    
     示例 2:
     输入:[1,2,3,4,5,6]
     输出:此列表中的结点 4 (序列化形式:[4,5,6])
     由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
      
    
     提示:
     给定链表的结点数介于 1 和 100 之间。
    
     力扣得分:
     执行用时 :108 ms, 在所有 JavaScript 提交中击败了19.44%的用户
     内存消耗 :33.6 MB, 在所有 JavaScript 提交中击败了74.60%的用户
     */
    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    /**
     * @param {ListNode} head
     * @return {ListNode}
     */
    var middleNode = function(head) {
    
        if (!head) return head;
    
        let arr = [];
        while(head){
            arr.push(head);
            head = head.next;
        }
    
        let len = arr.length;
        return len % 2 == 0 ? arr[len/2] : arr[(len-1)/2];
    };
    
    //测试
    var obj = new ListNode(1), temp = obj;
    for (let i = 0; i < 6; i++){
        temp.next = new ListNode(2+i);
        temp = temp.next;
    }
    console.log(obj);
    console.log("获取中间节点:")
    console.log(middleNode(obj));
    View Code

    参考代码demo2_快慢指针:

    /**
     * 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
     如果有两个中间结点,则返回第二个中间结点。
    
     示例 1:
     输入:[1,2,3,4,5]
     输出:此列表中的结点 3 (序列化形式:[3,4,5])
     返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
     注意,我们返回了一个 ListNode 类型的对象 ans,这样:
     ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
    
     示例 2:
     输入:[1,2,3,4,5,6]
     输出:此列表中的结点 4 (序列化形式:[4,5,6])
     由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
      
    
     提示:
     给定链表的结点数介于 1 和 100 之间。
    
     力扣得分:
     执行用时 :120 ms, 在所有 JavaScript 提交中击败了12.22%的用户
     内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了11.11%的用户
    
     官方答案,官方这个确实简洁:
     let slow = fast = head;
     while (fast && fast.next) {
            slow = slow.next;
            fast = fast.next.next;
        }
     return slow;
    
     官方力扣得分:
     执行用时 :64 ms, 在所有 JavaScript 提交中击败了99.44%的用户
     内存消耗 :34.1 MB, 在所有 JavaScript 提交中击败了11.11%的用户
    
     */
    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    
    function ListNode(val){
        this.val = val;
        this.next = null;
    }
    
    /** 用快慢指针来处理下
     * @param {ListNode} head
     * @return {ListNode}
     */
    var middleNode = function(head) {
        // let slow = head, fast = head;
        // while(slow){
        //     if (fast){
        //         fast = fast.next;
        //         if (fast){
        //             fast = fast.next;
        //         }
        //         else{
        //             return slow;
        //         }
        //     }
        //     else{
        //         return slow;
        //     }
        //     slow = slow.next;
        // }
        // return head;
    
        //官方答案:简洁明了
        let slow = fast = head;
        while (fast && fast.next) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    };
    
    //测试
    var obj = new ListNode(1), temp = obj;
    for (let i = 0; i < 6; i++){
        temp.next = new ListNode(2+i);
        temp = temp.next;
    }
    console.log(obj);
    console.log("获取中间节点:")
    console.log(middleNode(obj));
    
    obj = new ListNode(90), temp = obj;
    for (let i = 0; i < 5; i++){
        temp.next = new ListNode(91+i);
        temp = temp.next;
    }
    console.log(obj);
    console.log("获取中间节点:")
    console.log(middleNode(obj));
    View Code

    参考Demo地址:https://github.com/xiaotanit/Tan_DataStruct

  • 相关阅读:
    __iter__方法demo
    开放封闭原则
    单例模式
    Python赋值、浅拷贝、深拷贝
    保留原页面的参数条件
    request.GET、request.POST、request.body(持续更新)
    面向对象的封装、继承、多态(持续更新)
    关于Form、ModelForm的一些操作(持续更新)
    创建类的两种方式
    Nginx深度优化
  • 原文地址:https://www.cnblogs.com/tandaxia/p/11078405.html
Copyright © 2020-2023  润新知