算法练习day3

65 阅读4分钟

一、链表基础

链表是不同节点通过指针串联在一起的线性结构,每个节点存储了数据和指向某个节点的指针

链表包含单链表,双链表,循环链表

单链表的每个指针指向节点的下一个节点

双链表的每个节点包含了两个指针,分别指向上个节点和下一个节点

循环链表的首节点和尾节点互相连接

链表的增加和删除的复杂度都是O(1),查找的复杂度是O(n)

二、移除链表元素

问题要点

两种思路:

  1. 在使用原链表直接删除目标节点

这种思路下,头节点和其他节点的处理逻辑不同,因为头节点没有前面的节点,所以需要先循环判断处理头节点,然后处理剩余节点

如果当前节点值等于目标值,则pre节点跳过cur,指向cur.next

如果当前节点值不等于目标值,则pre节点更新为pre.next

每次遍历都处理cur = cur.next,更新当前节点

  1. 使用一个虚拟节点进行删除操作

这种思路下,头节点和其他节点可以使用同一个逻辑进行移除

判断当前节点cur的下个节点

如果节点值等于目标值,跳过cur.next,更新cur的下一个节点为cur.next.next

如果节点值不等于目标值,更新cur的下一个节点为cur.next

原地删除

/**
 * 原地删除
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    while(head && head.val === val) {
        head = head.next
    }
    if(!head) {
        return head
    }
    let pre = head
    let cur = head.next
    while(cur) {
        if(cur.val === val) {
            pre.next = cur.next
        } else {
            pre = pre.next
        }
        cur = cur.next
    }
    return head
};

虚拟节点方式

/**
 * 设置虚拟头节点
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    let dummyHead = new ListNode(0, head)
    let current = dummyHead
    while(current.next) {
        if(current.next.val === val) {
            // 删除后继续判断,current保持不变
            current.next = current.next.next
        } else {
            current = current.next
        }
    }
    return dummyHead.next
};

三、设计链表

问题要点

抽象一个方法getNode用来获取某个索引下的节点,方便后续方法使用

在新增,删除的方法中要注意正确更新头节点,尾节点和链表数量,注意控制边界条件,如操作的索引正好是头节点或尾节点的时候


var MyLinkedList = function() {
    this._size = 0
    this._head = null
    this._tail = null
};
/** 
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.getNode = function(index) {
    if(index < 0 || index >= this._size) {
        return null
    }
    let dummy_head = new ListNode(0, this._head)
    let cur = dummy_head
    while(index-- >= 0) {
        cur = cur.next
    }
    return cur
};

/** 
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function(index) {
    let res = this.getNode(index)
    return res === null ? -1 : res.val
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function(val) {
    let newHead = new ListNode(val, this._head)
    this._size++
    this._head = newHead
    if(!this._tail) {
        this._tail = newHead
    }
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function(val) {
    let newTail = new ListNode(val, null)
    if(!this._tail) {
        this._tail = newTail
        this._head = newTail
    } else {
        this._tail.next = newTail
        this._tail = newTail
    }
    this._size++
};

/** 
 * @param {number} index 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function(index, val) {
    if(index > this._size) {
        return
    }
    if(index ===this._size) {
        this.addAtTail(val)
    } else if(index <= 0) {
        this.addAtHead(val)
    } else {
        let node = this.getNode(index-1)
        node.next = new ListNode(val, node.next)
        this._size++
    }
    
};

/** 
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function(index) {
    if(index < 0 || index >= this._size) {
        return
    }
    if(index === 0) {
        this._head = this._head.next
        if(index === this._size - 1) {
            this._tail = null
        }
    } else {
        let node = this.getNode(index - 1)
        node.next = node.next.next
        if(index === this._size - 1) {
            this._tail = node
        }
    }
    this._size--
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * var obj = new MyLinkedList()
 * var param_1 = obj.get(index)
 * obj.addAtHead(val)
 * obj.addAtTail(val)
 * obj.addAtIndex(index,val)
 * obj.deleteAtIndex(index)
 */

四、反转链表

问题要点

有三种解题思路:

  1. 双指针法

pre指针表示上一个节点,cur指针表示当前节点,每次遍历先把下一个节点存到temp指针中,然后开改变指向,cur.next = pre, 然后更新pre为cur, cur为temp,继续遍历,知道cur为null,此时pre为反转后的头节点

  1. 递归法,从前往后

和双指针的转换思路一致,每次递归中都是,cur.next = pre。保证每次递归调用传入正确的pre,和cur,递归终止条件为cur为null,返回pre

  1. 递归法,从后往前

首先找到链表的尾节点,反转后将为头节点

然后递归找到尾节点,pre.next = head; head = null,递归调用

双指针法

/**
 * 双指针法
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let pre = null
    let cur = head
    let temp = null
    while(cur) {
        temp = cur.next
        cur.next = pre
        pre = cur
        cur = temp
    }
    return pre
};

递归法,从前往后

/**
 * 递归法(从前往后)
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    return reverse(null, head)
};
function reverse(pre, cur){
    if (!cur) {
        return pre
    }
    let temp = cur.next
    cur.next = pre
    return reverse(cur, temp)
}

递归法,从后往前

/**
 * 递归法(从后往前)
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let cur = head
    while(cur && cur.next) {
        cur = cur.next
    }
    reverse(head)
    return cur
};
function reverse(head){
    if(!head || !head.next) {
        return head
    }
    let pre = reverse(head.next)
    head.next = null
    pre.next = head
    return head
}