Fork me on GitHub

链表

链表不需要一块连续的内存空间来存储,它通过“指针”将一组零散的内存块串联起来使用
三种最常见的链表结构:单链表、双向链表和循环链表

单链表

为了将所有的结点串起来,每个链表的结点除了存储数据之外,还需要记录链上的下一个结点的地址

从图中我们可以看出,针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)

链表要想随机访问第 k 个元素,就没有数组那么高效了。因为链表中的数据并非连续存储的,所以无法像数组那样,根据首地址和下标,通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。链表随机访问的性能没有数组好,需要 O(n) 的时间复杂度。

设计一个基于对象的单链表

//Node 类用来表示节点
function Node(element) {
    this.element = element;
    this.next = null;
}
// 链表类
function LinkedList() {
    this.head = new Node("head");
    this.find = find;
    this.insert = insert;
    this.remove = remove;
    this.display = display;
    this.findPrevious = findPrevious;
    this.remove = remove;
}
//该方法遍历链表,查找给定数据
function find(item) {
    var currNode = this.head;
    while (currNode.element != item) {
        currNode = currNode.next;
    }
    return currNode;
}
//将新节点插入链表
function insert(newElement, item) {
    var newNode = new Node(newElement);
    var current = this.find(item);
    newNode.next = current.next;
    current.next = newNode;
}
// 输出当前链表
function display() {
    var currNode = this.head;
    while (!(currNode.next == null)) {
        console.log(currNode.next.element);
        currNode = currNode.next;
    }
}
// 查找待删除节点前面的节点
function findPrevious(item) {
    var currNode = this.head;
    while (!(currNode.next == null) && (currNode.next.element != item)) {
        currNode = currNode.next;
    }
    return currNode;
}
// 删除节点
function remove(item) {
    var prevNode = this.findPrevious(item);
    if (!(prevNode.next == null)) {
        prevNode.next = prevNode.next.next;
    }
}
var cities = new LinkedList();
cities.insert("Conway", "head");
cities.insert("Russellville", "Conway");
cities.insert("Carlisle", "Russellville");
cities.insert("Alma", "Carlisle");
cities.display();
cities.remove("Carlisle");
cities.display();

循环链表

循环链表的尾结点指针是指向链表的头结点

双向链表


双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。虽然两个指针比较浪费存储空间,但可以支持双向遍历


链表 VS 数组性能大比拼


和数组相比,链表更适合插入、删除操作频繁的场景,查询的时间复杂度较高。

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理。

if (head == null) { head = new_node;} //插入头结点
if (head->next == null) { head = null;} // 插入尾节点

重点留意边界条件处理

我经常用来检查链表代码是否正确的边界条件有这样几个:

  1. 如果链表为空时,代码是否能正常工作?
    2.如果链表只包含一个结点时,代码是否能正常工作?
    3.如果链表只包含两个结点时,代码是否能正常工作?
    4.代码逻辑在处理头结点和尾结点的时候,是否能正常工作?

5个常见的链表操作

  1. 单链表反转
    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    if(head == null || head.next == null){
        return head
    }
    const current = reverseList(head.next);
    //例如,1,2,3,4,5,null
    //current是5
    //head是4
    //head.next 是 5
    //head.next.next 就是5指向的指针,指向当前的head(4)
    //5-4-3-2-1-null
    head.next.next = head;
    //注意把head.next设置为null,切断4链接5的指针
    head.next = null
    //每层递归返回当前的节点,也就是最后一个节点。(因为head.next.next改变了,所以下一层current变4,head变3)
    return current;
};
  1. 链表中环的检测
  2. 两个有序的链表合并删除
  3. 链表倒数第 n 个结点
    给定一个链表: 1->2->3->4->5, 和 k = 2.
    返回链表 4->5.
    解题思路:用两个指针,让第一个先走k步,然后两个指针一起移动,当第一个指针到最后一个节点处,第二个指针就在倒数第K个节点
var getKthFromEnd = function(head, k) {
    let first = head,
        second = head;
    while (k !== 0) {
        first = first.next;
        k--;
    }
    while (first !== null) {
        first = first.next;
        second = second.next;
    }
    return second;
};
  1. 求链表的中间结点
posted @ 2020-04-19 16:32  Jesse131  阅读(259)  评论(0编辑  收藏  举报