03 Javascript数据结构与算法 之 链表

196 阅读5分钟

1. 定义

在栈和队列中,存储对象都是通过数组来实现的。但数组也有一个缺点:数组是定长的。如果从素组的起点或中间插入或移除数据,那么成本会很高(表面看不出来,但是实际是Array类方法在背后帮我们进行移动了)。

而链表存储有序的数据结构,不同于数组,链表的元素在内存中不需要连续放置,而是将相关的两个元素a、b,通过a节点的指针指向b节点的地址来进行引用。


链表好处在于新增元素,不需要移动元素,只需要将插入位置的前后元素的指针修改即可。


当移除元素也是一样的,将需要移除元素的前后节点 关联起来


2. linkList实例

链表是一个类,它包含了以下的方法属性:

  • 节点node是一个单独的对象,它应该拥有自己的属性,不可缺少的是next属性:指向下一个节点。
  • append(element): 向链表中添加node,添加到链表的尾巴上,但可能存在链表为空的情况
  • insert(position, element): 向指定位置插入新node,插入给定的位置,但可能存在插入位置是第一个位置
  • remove(element): 从列表中移除node
  • indexOf(element): 返回node在链表中的索引,没有则返回-1
  • removeAt(position): 移除特定位置的node,移除特定位置,但可能移除的位置是头部
  • isEmpty(): 链表不包含元素,则为空
  • size(): 链表的元素个数
  • toString(): 重写toString()方法,让其输出你想要的值
function LinkList() {
    // 定义链表对象
    // 定义链表长度
    // 当前遍历选择的对象
    let head, length, current;
     
    // 定义Node节点类
    function Node(element, next) {
        this.element = element;
        this.next = next;
    }
    this.append = function(element) {
       
        let node = new Node(element);
        // 如果链表中还没有对象,添加的节点就是第一个
        if (!head) {
            head = node;
        } else {
            // 获取链表最尾巴上的节点
            current = head;
            while (current.next) {
                current = current.next;
            }
            // 将新增节点添加到尾巴上
            current.next = node;
        }
        // 链表长度加1
        length++;
    };

    this.insert = function(position, element) {
        // position 不能小于0,并且不能大于链表长度
        if (0 > position || position > length) {
            return false;
        }
        let node = new Node(element);
        let index = 0, previous; // 记录current对象的索引,找到需要插入的位置
        current = head;
        // 如果插入位置是第一个位置,则直接将head赋值给node.next。并将node 设置为head
        if (position === 0) {
            node.next = head;
            head = node;
        } else {
            while(index++ < position) {
                // 记录current所在位置的前一个对象
                previous = current;
                // 设置current
                current = current.next;
            }
            // previous 应当是插入node的前一个节点,current应该是被插入node的后一个节点
            previous.next = node;
            node.next = current;
        }
        length++;
        return true;
    };

    this.removeAt = function(position) {
         // position 不能小于0,并且不能大于链表长度
        if (0 > position || position > length) {
            return null;
        }
        // 如果是移除头部
        if (position === 0) {
            head = head.next;
        } else {
            // 移除中间的或者尾部
            current = head;
            let index = 0, previous;
            // 找到需要移除的位置(current)
            while (index++ < position) {
                previous = current;
                current = current.next;
            }
            // 将current的前一个node节点的next属性指向current的下一个node节点
            previous.next = current.next;
        }
        length--;
        return current.element;
    };

    this.indexOf = function(element){
        var current = head,
        index = -1;
        while (current) { 
            if (element === current.element) {
                return index;
            }
            index++;
            current = current.next; 
        }
        return -1;
    };

    this.remove = function(element){
        var index = this.indexOf(element);
        return this.removeAt(index);
    };

    this.isEmpty = function() {
        return length === 0;
    };

    this.size = function() {
        return length;
    };
    this.toString = function() {
        console.log(head)
    }
};

3. 双向链表


双向链表和单向链表的区别在于:

  • 单向链表只能链向下一个节点,而双向链表中next属性链向下一个节点,prev属性链向上一个节点
  • insert(position, element): 不仅要控制next,还得控制prev指针。插入指定位置,但可能存在插入位置是第一个位置,最后一个位置



  • removeAt(postion): 同单项链表一样,只是多设置一个prev指针。移除特定位置,但可能移除的位置是头部



function DoudlyLinkedList()  {

    // Node节点增加prev属性,指向前一个node节点
    function Node(element) {
        this.element = element;
        this.next = null;
        this.prev = null;
    }

    // 新增tail,存放链表中最后一个节点
    let length = 0, head = null, tail = null, current = null;

    this.insert = function(element, position) {
        if (position < 0 || position > length) {
            return false;
        }
        // 需要插入的节点
        let node = new Node(element);

        // 如果插入节点是头节点
        if (position === 0) {
             current = head;
            // 第一次插入,没有node
            if (!head) {
                head = node;
                tail = node;
            } else {
                // 修改原head的prev 指向新node。 新node的next 等于原head
                // 并将新node设置为head
                node.next = current;
                current.prev = node;
                head = node;
            }
            // 插入位置为尾巴
        } else if (position === length) {
            current = tail;
            current.next = node;
            node.prev = current;
            tail = node;

        // 插入位置是任意中间位置
        } else {
            let previous = head;
            current = head;
            let i = 0;
            while (i++ < position) {
                previous = current;
                current = current.next;
            }
            // previous 是需要插入位置的前一个node, current是后一个节点
            node.prev = previous; // 新增的
            node.next = current;
            previous.next = node;
            current.prev = node; // 新增
        }

        length++;
        return true;
    };

    this.removeAt = function(position) {
        if (position > length || position < 0) {
            return null;
        }

        current = head;

        // 如果移除的是第一个
        if (position === 0 ) {
            
            head = current.next;
            // 如果只有一项,那么tail 也得设置为null
            if (length === 1) {
                tail = null;
            } else {
                // 将headprev索引设置为null
                head.prev = null;
            }

            // 移除的是末尾的元素
        } else if (position === length-1) {
            current = tail;
            tail = tail.prev;
            tail.next = null;
        } else {
            let index = 0, previous;
            while (index++ < position) {
                previous = current;
                current = current.next;
            }
            // 移除current,就是讲current前后两个节点联系在一起
            previous.next = current.next;
           current.next.prev = previous;
        }

        length--;
        return current.element;
    };

    this.toString = function() {
        console.log(head)
    }
}

4. 循环链表

循环链表 分为单项循环链表与双向循环链表。不再编写代码

  • 单项循环链表: 最后一个节点tailnext属性 指向 第一个节点head


  • 双向循环链表:最后一个节点tailnext属性指向第一个节点head。第一个节点headprev属性指向最后一个节点tail