js- 数据结构 - 链表(单向链表)

88 阅读3分钟

数据结构-链表

引言

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

数组:几乎每一种编程语言都有默认实现数组结构。他非常的方便,提供了便利的【】语法来访问元素,然后这种数据结构有一个大缺点:(在大多数语言中)数组的大小是固定的,从数组的起点或者中间插入或者移除的成本很高(牵一发而动全身!!!)

链表:链表存储有序的元素集合,但不同于数组,链表在内存中并不是连续放置的,每个元素由一个存储元素本身的节点指向下一个元素的引用也称指针组成。

链表的分类

image.png

image.png

image.png

image.png

单链表常见的一些方法

因为我们每次要创建节点(一个存储元素本身的值和一个指向下一个元素的引用next),所以可以封装一个通用的类,来生成节点

class Node {
    constructor(value) {
        this.value = value;
        this.next = null
    }
}

封装一个链表类,里面会有很多方法。链表最重要的是要知道头节点(因为没有索引,所有都要从头开始查找),再加上一个length,可以随时统计链表的长度,此时链表类的基本代码完成。

class LinkedList {
    constructor(value) {
        this.head = null
        this.length = 0
    }
}

findNode(value) 查找节点

  • 先拿到头节点,如果头节点和传入的值不相等,那么一直去找他的下一个值
  • 直到找到,返回当前节点。
findNode(value){
    const currentNode = this.head
    while(currentNode.value !== value) {
        currentNode = currentNode.next
    }
    if(!currentNode) {
        return null
     }
    return currentNode
}

getElementAt(value) 返回链表中指定位置的元素,如果链表中不存在这样的元素,则返回undefined

  • 边界判读
  • 定义一个变量,和传入的index做比较,相等的时候,就是找到了目标值
getElementAt(index) {
    if (index >= 0 && index < this.length) {
      let count = 0;
      let current = this.head;
      while (count++ < index) {
        current = current.next;
      }
      return current;
    }
    return undefined;
  }

indexOf(element): 返回元素在链表中的索引,如果链表中没有该元素则返回-1.

  • 只要next.value 和传入value不相等就一直循环,并且每次count++.
  • 如果next.value == value 证明找到了,则返回count
indexOf(value) {
    let current = this.head;
    let count = 0;
    while (current.value != value) {
      current = current.next;
      count++;
    }
    return count ? count : -1;
  }

push(value): 向链表尾部添加一个新的元素

  • 如果是空链表,那么直接把this.head 指向心节点即可
  • 如果不是新节点,那么在链表的末尾的节点(node.next) = newNode
push(value) {
    let newNode = new Node(value)
    let current = this.head
    if (this.head == null) { // 当然这里 用this.length == 0 来判断是否是空节点也可以
        this.head = newNode
    } else {
        while(current.next) {
            current = current.next
        }
        current.next = newNode
    }
    this.length++
}

remove(element): 从链表中移除一个元素

  • 如果传入的值和next.value 不想等,则一直循环。
  • 如果传入的值和next.value相等,则将上一个节点的next 指向当前节点的next,想单于跳过这个节点,完成删除操作
  • 但是删除的可能是头节点,头节点的上一个节点没有next 属性,当执行previous.next = current.next会报错,所以要检测是否是头节点(这里有很多种方式)
remove(value) {
    let current = this.head;
    let previous = null;

    while (current.value != value) {
      previous = current;
      current = current.next;
    }
    if (current == this.head) {
      // this.head = null
      this.head = current.next;
    } else {
      previous.next = current.next;
    }
    this.length--;
  }

removeAt(index): 从链表指定位置中移除一个元素

  • 边界判断
  • 由于有传入index,可以使用getElementAt 找到上一个节点
  • 然后再使用循环的方式,找到当前节点
  • 将上一个节点的next 指向下一个节点的next,完成删除操作
  • 最后length--
removeAt(index) {
    if (index >= 0 && index < this.length) {
      let previous = this.getElementAt(index - 1);
      let current = this.head;
      let count = 0;
      while (count++ < index) {
        previous = current;
        current = current.next;
      }

      previous.next = current.next;
      this.length--;
    }
  }

insert(index,value): 从链表任意位置插入元素

  • 边界判断
  • 由于有传入index,可以使用getElementAt 找到上一个节点
  • 由于传入当前节点,所以不用循环去获取,直接new Node(value)设置节点
  • 当前节点的next 指向上一个节点的next
  • 上一个节点的next 指向 当前节点
  • 最后length++
insert(index, value) {
    if (index >= 0 && index < this.length) {
      let previous = this.getElementAt(index - 1);
      let current = new Node(value);
      current.next = previous.next;
      previous.next = current;

      this.length++;
    }
  }

isEmpty(): 如果链表中不包含任何元素,则返回true。如果链表长度大于1,则返回false

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

size(): 返回链表中包含的元素个数,与数组的length 类似

size() {
    return this.length
 }

测试

let list = new LinkedList();
list.push(4); //4
list.push(5); // 4->5
list.push(6); // 4->5->6
list.push(7); //4->5->6-7
list.insert(1, 9); // 4->9->5->6-7
list.remove(5); // 删除节点5  4->9->6-7
list.removeAt(1); // 删除index 为1的节点,也就是4  4->6-7
console.dir(list, {
  depth: 100,
});

let indexOf = list.indexOf(7);
console.log('indexOf', indexOf); // 2
let getElementAt = list.getElementAt(1);
console.log('getElementAt', getElementAt.value); //6

image.png

总结

  • 链表的一些操作,要进行边界处理,比如传入的index是否是大于0 ,并且是小于链表的长度
  • 书写常用方法时,可以写一下工具函数,类似于findNode,indexOf, getElementAt,可以大大提效
  • 如果传入index,找元素,那么就维护一个变量,每次和index 做比较,如果相等,则说明找到了index 所对应的节点
  • 如果传入的是一个value值,对于查找类型,每次和current.value 做比较即可
  • 对于插入类型,把当前值包装成节点,就不用循环查找节点了,利用getElementAt获取到前一个节点,和当前值做一个简单的next 交替即可完成insert 操作。
  • 代码的形式可以是多样的,可以使用 this.head == null / this.length == 0 来判断是否是空节点。比如删除方法中的这段代码:
if (current == this.head) {
  // this.head = null
  this.head = current.next; // 这就是循环了半天,发现就自己一个节点。
  //那还犹豫什么,自己把自己的head 设置为null不就完事了,非得用current.next。
  //因为没有下一个,current.next也是null。
  // 或者在开始就判断,如果只有一个节点,this.head直接设置为null。
  //就不走下面的逻辑了

不要拘泥于别人的实现,按照自己的思路,代码跑通即可。