链表

155 阅读3分钟

链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。下图展示了一个链表的结构

image.png

相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需的元素。

简而言之,链表相对于数组方便删除、增加 ,但由于结构的特点,导致查询是需要从头开始。

由于需要对节点查找,我们需要每次都对比两个节点是否相同,为了复用,需要将这个方法抽离出来。

// util.js
export function defaultEquals (a, b) {
    return  a === b
}

对于每一个节点都是都是相互独立的,使用我们需要实现一个Node Class,这个类具有两个属性, Node.element代表元素的本身,Node.next代表下一个元素/链表尾部。

注意:当知道某一个元素,也就同时知道了它的下一个元素。

// lined-list-models.js
export class Node {
    constructor (element) {
        this.element = element 
        this.next = undefined
    }
}

现在实现链表,以及相应的方法。

// linkList.js
import { defaultEquals } from "./util";
import { Node } from "./linked-list-models";

export default class LinkedList {
    constructor (equalsFn = defaultEquals) {
        this.head = undefined
        this.count = 0
        this.equalsFn = equalsFn
    }
    // 链表尾部插入
    push ( element ) { }
    // 指定位置插入新元素
    insertAt ( element,  position ) { }
    // 返回指定位置的元素,不存在返回undefined
    getElementAt ( index ) { }
    // 移除指定元素,
    remove (element) { }
    // 查找指定元素的位置,不存在返回-1
    indexOf ( element ) { }
    // 删除指定位置的元素
    removeAt ( position ) { }
    //  判断链表是否为空,返回 true / false
    isEmpty () { }
    // 返回链表的长度
    size () { }
    // 链表字符串化
    toSting () { }
}

1.链表尾部的插入

在向尾部插入的时候需要考虑两种清空,一种是链表为空,一种是链表不为空。不为空的时候我们需要遍历到最后一个元素,然后把新元素链继到它的next(lastNode.next = newNode)。而为空的时候,我们需要把链表的head指向该元素。

      // 链表尾部插入
    push ( element ) { 
        const node =  new Node( element )
        let current
        if( this.head === undefined ) { // 链表为空
            this.head = node 
        } else {
            current = this.head
            while (current.next !== undefined) {
                current = current.next
            }
            current.next = node
        }
        this.count ++ 
    }
     

2.指定位置删除元素

在指定位置删除时,需要考虑输入的位置是否合法。然后分2种情况,第一张是删除第一个元素,第二是删除其他元素。 删除的具体思路是,找到需要被删除的当前元素(currentNoode)和它的上一个元素(previousNode),在知道当前元素的时候,也代表了知道它的下一个元素(currentNode.next),然后我们把prviousNode.next指向currentNode.next即可。删除头元素只需要把header 指向 current.next

  • 头部删除元素
    image.png

  • 中间删除元素 111.png

  • 尾部删除元素

image.png

    // 删除指定位置的元素
    removeAt ( position ) { 
        if ( position >= 0 &&  position<this.count ) {
            let current = this.head
            if (position === 0) {
                this.head = current.next
            } else {
                 let previous
                 for (let index = 0; index < position; index++) {
                     previous = current
                     current = current.next
                 }
                 previous.next = current.next
            }
            this.count --
            return current.element   
        }
        return undefined
    }

由于每次查找都需要遍历元素,所以这里我们可以将方法封装

3. 查找指定位置元素

       // 返回指定位置的元素,不存在返回undefined
    getElementAt ( position ) { 
        if( position >=0 &&  position < this.count){
            let node = this.head
            for (let index = 0; index < position && node !== undefined; index++) {
                node = node.next
            }
            return node
        }
        return undefined
    }

现在可以将 removeAt方法改造

    // 删除指定位置的元素
    removeAt ( position ) { 
        if ( position >= 0 &&  position<this.count ) {
            let current = this.head
            if (position === 0) {
                this.head = current.next
            } else {
                 let previous = this.getElementAt( position-1 )
                 current = previous.next
                 
                //  for (let index = 0; index < position; index++) {
                //      previous = current
                //      current = current.next
                //  }
                 previous.next = current.next
            }
            this.count --
            return current.element   
        }
        return undefined
    }

4. 在任意位置插入元素

这里需要考虑两种情况,一种是在第一个,一种是插入在其他位置。当插入的位置为第一个是,head的指向需要改到刚插入的元素。

    // 指定位置插入新元素
    insertAt ( element,  position ) { 
        if( position >=0 &&  position < this.count){
            let node = new Node(element)
            if (position === 0 ){
                let current = this.head
                node.next = current
                node = this.head
            } else {
                let previous = this.getElementAt ( position - 1)
                let current =previous.next
                previous.next = node
                node.next = current
            }
            this.count ++
            return true
        }
        return false
    }

5.返回元素的位置

这里依然需要遍历链表。

    indexOf ( element ) { 
        let current = this.head
        for (let index = 0; index < this.count && this.count !== undefined; index++) {
            if ( current === element ){
                return index
            }
            current = current.next
        }
        return -1
    }

6. 删除指定的元素

我们可以先通过 indexOf 来找到元素的位置,再通过 removeAt在指定的位置删除元素。

    // 移除指定元素,
    remove (element) { 
        let position = this.indexOf (element)
       return  this.removeAt (position)
    }

7. toSting 方法

    // 链表字符串化
    toSting () { 
        if (this.head == null) { 
                return ''; 
            } 
            let objString = `${this.head.element}`; 
            let current = this.head.next; 
            for (let i = 1; i < this.size() && current != null; i++) { 
                objString = `${objString},${current.element}`; 
                current = current.next; 
            } 
            return objString;
    }