单链表问题

  1. 单链表节点定义

class Node {
    public ele: any;
    public next: null;

    constructor(ele) {
        this.ele = ele;
        this.next = null;
    }
}

2. 单链表基本操作

  • 插入 `insert(ele, pos)`

  • 查询 `get(pos)`

  • 删除 `remove(pos)`

  • 获取头节点 `getHead()`

  • 获取链表长度 `getSize()`

  • 判断链表是否为空 `isEmpty()`

  • 获取链表节点数组 `getList()`

3. 基本操作实现

class LinkedList {
    private head: any;
    private length: number;

    constructor() {
        this.head = null;
        this.length = 0;
    }

    /**
     * 查询指定位置节点
     * @param pos 待搜索节点位置
     * @returns {Node} 找到返回节点,否则返回-1
     */
    get(pos) {
        if (pos < 0 || pos > this.length) {
            console.warn("索引位置非法!");
            return -1;
        }
        let tmp = this.head,
            index = 0;
        while (index++ < pos) {
            tmp = tmp.next;
        }
        return tmp;
    }

    /**
     * 追加节点
     * @param {Node} ele 待插入节点
     * @returns {Boolean} 插入成功返回true,否则false
     */
    append(ele) {
        let newNode = new Node(ele);

        if (!this.head) {
            this.head = newNode;
        } else {
            let tmp = this.head;
            while (tmp.next) {
                tmp = tmp.next;
            }
            tmp.next = newNode;
        }
        this.length++;
        return true;
    }

    /**
     * 指定位置插入节点
     * @param {Node} ele 待插入节点
     * @param {Number} pos 待插入位置  默认为length
     * @returns {Boolean} 插入成功返回true,否则false
     */
    insert(ele, pos?) {
        let validPos = pos ?? this.length; // pos为null/undefined是取右边的值
        if (validPos < 0 || validPos > this.length) {
            console.warn("插入位置非法!");
            return false;
        }

        let newNode = new Node(ele),
            tmp = this.head,
            index = 0,
            prev;

        if (validPos == 0) {
            newNode.next = tmp;
            // 更新头节点
            this.head = newNode;
        } else {
            while (index++ < validPos) {
                prev = tmp;
                tmp = tmp.next;
            }
            // console.log('test', prev, tmp);
            // newNode要插在prev和tmp之间
            // newNode下一个指向tmp
            // prev下一个指向newNode
            // 从而把链条拼接上
            newNode.next = tmp;
            prev.next = newNode;
        }
        this.length++;
        return true;
    }

    /**
     * 指定位置删除节点
     * @param {Number} pos 待删除位置
     * @returns {Boolean} 删除成功返回true,否则false
     */
    remove(pos) {
        if (pos < 0 || pos > this.length) {
            console.warn("删除位置非法!");
            return false;
        }

        // 找到要删除节点以及其之前的节点
        let tmp = this.head,
            index = 0,
            prev;
        if (pos === 0) {
            this.head = this.head.next;
        } else {
            while (index++ < pos) {
                prev = tmp;
                tmp = tmp.next;
            }
            // console.log('test', prev, tmp);
            // 直接跳过tmp,和tmp的下一个节点拼接
            prev.next = tmp.next;
        }
        this.length--;
        return true;
    }

    /**
     * 链表是否为空
     * @returns {Boolean} 链表为空返回true,否则false
     */
    isEmpty() {
        return this.length === 0;
    }

    /**
     * 获取头节点
     * @returns {Node} 返回头节点
     */
    getHead() {
        return this.head;
    }

    /**
     * 获取链表长度
     * @returns {Number} 返回链表长度
     */
    getSize() {
        return this.length;
    }

    /**
     * 获取链表元素数组
     * @returns {Array} 返回链表元素数组
     *
     * TODO 需要考虑环状链表
     */
    getList() {
        const ret = [];
        let cur = this.head;
        while (cur.next) {
            ret.push(cur.ele);
            cur = cur.next;
        }
        ret.push(cur.ele);
        return ret;
    }
}

4. 单链表高级操作

4. 1 链表合并

将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的

const mergeTwoList = (r1, r2) => {
    if (r1 === null) return r2;
    if (r2 === null) return r1;

    if (r1.ele <= r2.ele) {
        // 指针后移
        r1.next = mergeTwoList(r1.next, r2)
        return r1
    } else {
        r2.next = mergeTwoList(r2.next, r1)
        return r2
    }
}

4. 2 链表环检测

给定一个链表,判断链表中是否有环

方法1: 标记法

遍历链表,给已遍历过的节点加标志位

const hasCycle = (r) => {
     while (r) {
         if (r.visited) return true
         r.visited = true
         r = r.next;
     }
     return false
}

方法2:`JSON.stringify`

`JSON.stringify`序列化含有循环引用结构会报错

const hasCycle = (r) => {
     try {
         JSON.stringify(r);
         return false
     } catch (e) {
         return true
     }
}

方法3:双指针

快慢指针: 遍历单链表,快指针一次走两步,慢指针一次走一步

如果单链表中存在环,则快慢指针终会指向同一个节点,否则直到快指针指向 null 时,快慢指针都不可能相遇

const hasCycle = (r) => {
    if (!r || !r.next) return false
    let fast = r.next.next, slow = r.next;

    while (fast !== slow) {
        if (!fast || !fast.next) return false
        fast = fast.next.next
        slow = slow.next
    }
    // 如需找到环入口只需新建一个指针tmp和slow指针同时向前遍历,当两者相等时,入口就是tmp
    return true
}

4.3 链表中间元素

求链表的中间结点,节点个数为偶数2n时返回n+1所在节点

快慢指针: 遍历单链表,快指针一次走两步,慢指针一次走一步

快指针走到尾节点的时候就是满指针走到中间的时候

const getMiddle = (r) => {
    if (!r || !r.next) return r;
    let fast = r, slow = r;
    while (fast && fast.next) {
        fast = fast.next.next;
        slow = slow.next
    }
    return slow
}

4.4 链表反转

方法1: 迭代法(三指针法)

使用三个指针prev, cur和next实现

next在遍历链表的时候临时保存cur的下一个节点

prev初始值为null,遍历链表的时候反转cur的指向到prev

prev和cur分别前移

const reverse = (r) => {
    if (!r || !r.next) return r;
    let prev = null, cur = r;
    while (cur) {
        // 用于临时存储 cur 后继节点
        // 否则下一步就会抹掉原来的cur.next
        let next = cur.next;  // 4

        // 反转 cur 的后继指针
        cur.next = prev;  // 首个节点的next就变为了null

        // 变更prev、cur
        prev = cur;  // prev前移
        cur = next  // cur前移
    }
    r = prev;
    return r;
}

方法2: 递归

const reverse = (r) => {
    const _reverse = function (prev, cur) {
        if (!cur) return prev  // 前移的时候cur为null就结束了
        // 临时存储
        let next = cur.next;
        // 核心反转
        cur.next = prev;
        return _reverse(cur, next)  // prev,cur => cur, next
    }

    if (!r || !r.next) return r;

    r = _reverse(null, r);
    return r;
}

4.5 链表求和

给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。

const addTwoNumbers = (l1, l2) => {
    let carry = 0;
    let head = null, tail = null;  // 当carry>0时,tail用于表示进位

    while (l1 || l2) {
        let n1 = l1 ? l1.ele : 0;
        let n2 = l2 ? l2.ele : 0;
        let sum = n1 + n2 + carry;

        if (!head) {
            head = tail = new Node(sum % 10);
        } else {
            tail.next = new Node(sum % 10);
            tail = tail.next
        }

        carry = (sum / 10) >> 0

        // console.log('ele', sum, l1.ele, l2.ele)
        if (l1) {
            l1 = l1.next;
        }
        if (l2) {
            l2 = l2.next;
        }
    }

    // 如果有进位,附加一个新节点,其值为carry
    if (carry) {
        tail.next = new Node(carry)
    }
    return head
}

4.6 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。

方法1:遍历链表将节点值放入数组,然后双指针 复杂度:O(n) O(n)

var isPalindrome = function (head) {
  const arr = [];
    while (head) {
      arr.push(head.val);
      head = head.next;
    }
    let i = 0,
      len = arr.length,
      j = len - 1;
    for (i, j; i < j; i++, j--) {
      if (arr[i] !== arr[j]) {
        return false;
      }
    }
    return true;
};

方法2:前半部分指针反转一下,然后再判断回文 复杂度:O(n) O(1)

var isPalindrome = function (head) {
  let slow = head,
    fast = head;
  let prev = null,
    next = null;

  while (fast !== null && fast.next !== null) {
    fast = fast.next.next;
    next = slow.next;
    // slow 边走边逆序
    slow.next = prev;
    prev = slow;
    slow = next;
  }

  if (fast !== null) {
    // 链表长度为奇数
    slow = slow.next;
  }

  while (prev !== null && slow !== null) {
    if (prev.val !== slow.val) {
      return false;
    }

    prev = prev.next;
    slow = slow.next;
  }
  return true;
};

4.7 链表排序

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

var sortList = function (head) {
  // 自顶向下归并排序
  // 1. 找到链表的中点,以中点为分界,将链表拆分成两个子链表。
  //      寻找链表的中点可以使用快慢指针的做法,快指针每次移动 22 步,慢指针每次移动 11 步,当快指针到达链表末尾时,慢指针指向的链表节点即为链表的中点
  // 2. 对两个子链表分别排序。
  // 3. 将两个排序后的子链表合并,得到完整的排序后的链表

  return toSortList(head, null);
};
var toSortList = function (head, tail) {
  if (!head || !head.next) {
    return head;
  }

  if (head.next === tail) {
    head.next = null;
    return head;
  }
  //   let slow = head.next;
  //   let fast = head.next.next;
  //   while (fast && fast.next && fast !== tail) {
  //     slow = slow.next;
  //     fast = fast.next.next;
  //   }
  let slow = head,
    fast = head;
  while (fast !== tail) {
    slow = slow.next;
    fast = fast.next;
    if (fast !== tail) {
      fast = fast.next;
    }
  }

  const mid = slow;
  return mergeTwoList(toSortList(head, mid), toSortList(mid, tail));
};

Last updated