今天主要考察链表的理论基与基本操作。
JS 中定义链表节点
class ListNode {
val;
next = null;
constructor(value) {
this.val = value;
this.next = null;
}
}
思路:为了统一逻辑处理,创建虚拟头节点来删除第一个元素。链表删除当前节点下一个节点操作:cur.next = cur.next.next。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var removeElements = function(head, val) {
const temp = new ListNode(0, head)
let cur = temp
// console.log(cur.next)
while(cur.next!==null){
if(cur.next.val === val){
cur.next = cur.next.next
continue
}
cur = cur.next
}
return temp.next
};
实现需求:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
思路 :这道题没思路,较难,主要考察链表的增删改查操作。要考虑插入,删除时 链表中首尾节点的变化。
关键点:1)根据index获取对应节点可单独封装成一个函数,方便复用。
2)根据index删除元素时,考虑index为0时,链表首节点和尾节点的影响。考虑index为除0外的元素时,对链表尾节点的影响。
3)链表构造函数在定义时初始化了首尾指针和链表大小。
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
var MyLinkedList = function() {
this._size = 0
this._tail = null
this._head = null
};
//获取index对应节点
MyLinkedList.prototype.getNode = function(index) {
if(index < 0 || index >= this._size) return null;
// 创建虚拟头节点
let cur = new LinkNode(0, this._head);
// 0 -> head
while(index-- >= 0) {
cur = cur.next;
}
return cur;
};
/**
* @param {number} index
* @return {number}
*/
MyLinkedList.prototype.get = function(index) {
if(index < 0 || index >= this._size) return -1
return this.getNode(index).val;
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtHead = function(val) {
const node = new LinkNode(val , this._head)
this._head = node
this._size++
if (!this._tail){
this._tail = node;
}
};
/**
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtTail = function(val) {
const node = new LinkNode(val , null)
this._size++
if(this._tail){//尾节点不为空
this._tail.next = node
this._tail = node //node为新尾节点
return
}
//不存在尾结点
this._tail = node
this._head = node //该链表头尾节点都为node
};
/**
* @param {number} index
* @param {number} val
* @return {void}
*/
MyLinkedList.prototype.addAtIndex = function(index, val) {
if(index > this._size) return;
if(index <= 0) { //链表中没有节点
this.addAtHead(val);
return;
}
if(index === this._size) {//插入位置为尾部
this.addAtTail(val);
return;
}
//找到index-1 那个节点
const node = this.getNode(index - 1);
const cur = new LinkNode(val,node.next) //新节点指针指向前一个节点的next
node.next = cur //插入新节点
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 = this._head
}
this._size--;
return;
}
// 获取目标节点的上一个的节点
const 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)
*/
链表经典面试题:迭代法与递归法
思路:
双指针迭代,pre = null ,cur = head
- 用 temp 保存 cur.next 节点信息
- 将 cur.next 指向 pre ,pre始终为cur前的那个节点
- pre = cur , pre先向后移动,指向cur当前位置
- cur = temp ,cur再在后移动,指向cur的下一个为位置。
为什么pre先动,cur后动?
pre需要移动到cur的当前位置,cur先动了就无法记录当前cur的位置。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let pre = null,node = null
let cur = head
while(cur){
node = cur.next //保存cur的下一个节点
cur.next = pre //指针反向
pre = cur //先移动pre
cur = node //cur向下移动
}
return pre
}
递归写法:
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
// 递归:
var reverse = function(pre, head) {
if(!head) return pre;
const temp = head.next;
head.next = pre;
pre = head
return reverse(pre, temp);
}
var reverseList = function(head) {
return reverse(null, head);
};
今日总结:
主要考察链表基础算法。
第一题ac,注意虚拟头节点的使用。
第二题设计链表较难,第一次见没思路,考察单链表的五种基本操作,注意首尾节点的细节处理以及函数复用(根据index获取某个节点)。
第三题,面试高频,迭代和递归都需掌握,左右指针思想,注意节点指向会带来的链表变化,改变节点指向时,需要提前保存当前指向的节点,防止丢失链表指向。