欢迎来到力扣刷题笔记的第三部分 —— 超极高频的链表!
总路线戳这里~
707.设计链表【双链表专题记得再来写双链表方法】
练习设计自己的单链表~
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。
val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index)
:获取链表中第index
个节点的值。如果索引无效,则返回-1
。addAtHead(val)
:在链表的第一个元素之前添加一个值为val
的节点。插入后,新节点将成为链表的第一个节点。addAtTail(val)
:将值为val
的节点追加到链表的最后一个元素。addAtIndex(index,val)
:在链表中的第index
个节点之前添加值为val
的节点。- 如果
index
等于链表的长度,则该节点将附加到链表的末尾。 - 如果
index
大于链表长度,则不会插入节点。如果index
小于0,则在头部插入节点。
- 如果
deleteAtIndex(index)
:如果索引index
有效,则删除链表中的第index
个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
解题思路
就挨个功能实现咯
设计题考察对数据结构基本操作的熟练哈~
官方题解中提示的面试要点如下:
注意两个要点
- 哨兵节点 sentinel node
哨兵节点在树和链表中被广泛用作伪头(pseudo-head)、伪尾等,通常不保存任何数据。
我们将使用伪头来简化我们的插入和删除。我们将在在单链表、双链表中应用此方法。
- 双链表的双向搜索
我们可以从头部或尾部进行搜索。
另外需要注意本题的index是从0开始算起的(从示例可以看出 第0个节点 第1个节点…)
Java代码
单链表
public class ListNode{
//以下为单链表中结点的典型定义
int val;
ListNode next;
ListNode(int x){
val = x;
}
}
class MyLinkedList {
/** Initialize your data structure here. */
//以下是对单链表的实现
int size;//size存储链表中元素的个数 进行插入操作size++ 进行删除操作size--
ListNode head;//哨兵节点
//哨兵节点被用作伪头始终存在,这样结构中永远不为空,它将至少包含一个伪头。
public MyLinkedList(){
//MyLinkedList 中所有节点均包含:值 + 链接到下一个元素的指针。
size = 0;
head = new ListNode(0);
}
/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
//获取链表中第 index 个节点的值(从0算起)
public int get(int index) {
if(index < 0 || index >= size) return -1;
ListNode ans = head;
//接下来遍历寻找要get的节点
for(int i = 0; i < index + 1; i++){
ans = ans.next;
}
return ans.val;
}
/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
public void addAtHead(int val) {
addAtIndex(-666, val);//传入的index值只要<=0 即可插入到头节点
}
/** Append a node of value val to the last element of the linked list. */
public void addAtTail(int val) {
addAtIndex(size, val);//传入的index值 >= size 即可插入到尾节点
}
/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
//单链表的插入操作
public void addAtIndex(int index, int val) {
//如果index大于链表长度 返回空 index小于0 添加到头节点
if (index > size) return;
if(index < 0) index = 0;
size++;
//先设置 然后找到插入位置节点的前驱节点prev 注意这里是一个模板!
ListNode prev = head;//初始化prev为虚拟头节点head 在链表为空时进行插入删除/插入头节点操作时将更好理解
for(int i = 0; i < index; i++){
prev = prev.next;
}
ListNode toAdd = new ListNode(val);
//01 先让要插入的节点指向prev的下一个节点 也就是插入过后toAdd的下一个节点
toAdd.next = prev.next;
//02 然后prev再指向toAdd
prev.next = toAdd;
}
/** Delete the index-th node in the linked list, if the index is valid. */
//单链表的删除操作
public void deleteAtIndex(int index) {
if(index < 0 || index >= size) return;
size--;
//和上面添加的操作类似 找到要删除的那个节点的前一个 之后prev跨过要删除的节点直接指向.next.next即可
ListNode prev = head;
for(int i = 0; i < index; i++){
prev = prev.next;
}
prev.next = prev.next.next;
}
}
双链表
双链表是最常用的一种方法
因为它提供了——
- 常数时间内的
addAtHead
和addAtTail
操作 - 而单链表的
addAtHead
是常数时间的 但是由于没有设置虚拟尾节点 所以addAtTail
方法是线性时间的( O(n) )
public class ListNode{
//以下为双链表中结点的典型定义
int val;
ListNode next;
ListNode prev;//下一个节点 的意思
ListNode(int x){
val = x;
}
}
class MyLinkedList {
/** Initialize your data structure here. */
//以下是对单链表的实现
int size;//size存储链表中元素的个数 进行插入操作size++ 进行删除操作size--
ListNode head, tail;//哨兵节点 用作虚拟头节点head 和 虚拟尾节点tail
//哨兵节点被用作伪头始终存在,这样结构中永远不为空,它将至少包含一个伪头。
public MyLinkedList(){
//MyLinkedList 中所有节点均包含:值 + 链接到下一个元素的指针。
size = 0;
head = new ListNode(0);
tail = new ListNode(0);//虚拟尾节点
head.next = tail;
tail.prev = head;
}
/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
//获取链表中第 index 个节点的值(从0算起)
public int get(int index) {
// if index is invalid
if (index < 0 || index >= size) return -1;
// choose the fastest way: to move from the head
// or to move from the tail
ListNode curr = head;
if (index + 1 < size - index)
for(int i = 0; i < index + 1; ++i) curr = curr.next;
else {
curr = tail;
for(int i = 0; i < size - index; ++i) curr = curr.prev;
}
return curr.val;
}
public void addAtHead(int val) {
ListNode pred = head, succ = head.next;
++size;
ListNode toAdd = new ListNode(val);
toAdd.prev = pred;
toAdd.next = succ;
pred.next = toAdd;
succ.prev = toAdd;
}
public void addAtTail(int val) {
ListNode succ = tail, pred = tail.prev;
++size;
ListNode toAdd = new ListNode(val);
toAdd.prev = pred;
toAdd.next = succ;
pred.next = toAdd;
succ.prev = toAdd;
}
//单链表的插入操作
public void addAtIndex(int index, int val) {
//如果index大于链表长度 返回空 index小于0 添加到头节点
if (index > size) return;
if(index < 0) index = 0;
//先设置 然后找到插入位置节点的前驱节点pred 注意这里是一个模板!
ListNode pred, succ;
if(index < size - index){
//插入点在前半部分
pred = head;
for(int i = 0; i < index; i++){
pred = pred.next;
}
succ = pred.next;
}
else{
//插入点在后半部分 速度加快了!
succ = tail;
for(int i = 0; i < size - index; i++){
//往前倒 找插入位置的上一个节点
succ = succ.prev;
}
pred = succ.prev;
}
size++;
ListNode toAdd = new ListNode(val);
//双链表的插入方式
toAdd.prev = pred;
toAdd.next = succ;
pred.next = toAdd;
succ.prev = toAdd;
}
/** Delete the index-th node in the linked list, if the index is valid. */
//单链表的删除操作
public void deleteAtIndex(int index) {
// if the index is invalid, do nothing
if (index < 0 || index >= size) return;
// find predecessor and successor of the node to be deleted
ListNode pred, succ;
if (index < size - index) {
pred = head;
for(int i = 0; i < index; ++i) pred = pred.next;
succ = pred.next.next;
}
else {
succ = tail;
for (int i = 0; i < size - index - 1; ++i) succ = succ.prev;
pred = succ.prev.prev;
}
// delete pred.next
--size;
pred.next = succ;
succ.prev = pred;
}
}
JS代码
这里参考的 「代码随想录」带你搞定链表!707. 设计链表:【链表基础题目】详解
整体的思路很清晰 还附带了链表基础知识、虚拟头节点的总结
class LinkNode {
constructor(val, next) {
this.val = val;
this.next = next;
}
}
/**
* Initialize your data structure here.
* 单链表 储存头尾节点 和 节点数量
*/
var MyLinkedList = function() {
this._size = 0;
this._tail = null;
this._head = null;
};
/**
* Get the value of the index-th node in the linked list. If the index is invalid, return -1.
* @param {number} index
* @return {number}
*/
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;
};
MyLinkedList.prototype.get = function(index) {
if(index < 0 || index >= this._size) return -1;
// 获取当前节点
return this.getNode(index).val;
};
/**
* Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
* @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;
}
};
/**
* Append a node of value val to the last element of the linked list.
* @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;
return;
}
this._tail = node;
this._head = node;
};
/**
* Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
* @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;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = new LinkNode(val, node.next);
this._size++;
};
/**
* Delete the index-th node in the linked list, if the index is valid.
* @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;
this._size--;
return;
}
// 获取目标节点的上一个的节点
const node = this.getNode(index - 1);
node.next = node.next.next;
// 处理尾节点
if(index === this._size - 1) {
this._tail = node;
}
this._size--;
};
双链表
法一
双链表比单链表快得多 但是它更加复杂,它包含了 size
,记录链表元素个数,和伪头伪尾。
这个效率较低。。。
// 定义数据结构
var MyLinkedList = function() {
this.ListNode = function(val, prev, next){
this.val = val;
this.prev = prev;
this.next = next;
}
// 添加了 size 记录 结点个数
this.size = 0;
// 创建虚拟节点
this.dummyHead = new this.ListNode()
};
// 获取链表结点的值
MyLinkedList.prototype.get = function(index) {
if(index > this.size - 1 || index < 0){//这里注意要遍历到目标结点的前一个元素且index从0算起(题目要求) 所以这里要>this.size-1 下面要用虚拟头结点
return -1;
}
let node = this.dummyHead;//从虚拟头结点开始往下找结点的值
for(var i = 0; i < index; i++){
node = node.next;
}
return node.next.val;
};
// 在头部加入元素 (如果index小于0,则在头部插入节点
MyLinkedList.prototype.addAtHead = function(val) {
this.addAtIndex(0, val);//直接用下面写好的函数
};
// 在尾部加入元素 (如果 index 等于链表的长度,则该节点将附加到链表的末尾
MyLinkedList.prototype.addAtTail = function(val) {
this.addAtIndex(this.size, val);
};
// 插入结点
MyLinkedList.prototype.addAtIndex = function(index, val) {
if(index > this.size){
return;//如果 index 大于链表长度,则不会插入节点
}
let node = this.dummyHead;
for(var i = 0; i < index; i++){//要遍历到目标结点的前一个元素 然后执行插入操作
node = node.next;
}
let next = node.next;//这个就是要插入的那个结点
node.next = new this.ListNode(val, node, next);// 使用上面的函数 + 输入的数据(val node(前一个结点) next)创造当前结点
if (next){
next.prev = node.next;//把双链表连上
}
this.size++;
};
// 删除结点
MyLinkedList.prototype.deleteAtIndex = function(index) {
if(index > this.size - 1 || index < 0){
return;
}
let node= this.dummyHead;
for(var i = 0; i < index + 1; i++){//这里因为要删除当前结点 所以要遍历到当前节点的位置
node = node.next;
}
node.prev.next = node.next;//双链表的prev字段还是很方便的!
if(node.next){
node.next.prev = node.prev;
}
this.size--;
};
法二
这个方法效率较高
但是解题的方法比较个性化——自己创建了一个功能类 用于实现链表的这些功能
function ListNode(val, next = null, prev = null) {
this.val = val
this.next = next
this.prev = prev
}
class MyLinkedList {
constructor () {
this.data = new ListNode('head', new ListNode('tail'))
this.data.next.prev = this.data
this.len = 0
}
operate (index) { // 获得操作节点,主要定位到所需要修改的节点,的前面的节点!!
let cur = this.data
while (index-- > 0) {
cur = cur.next
}
return cur
}
get (index) {
if (index < 0 || index >= this.len) {
return -1
}
const operateNode = this.operate(index)
return operateNode.next.val
}
addAtHead (val) {
const cur = new ListNode(val, this.data.next, this.data) // 建立头部节点
this.data.next = cur // 正向链接
this.data.next.next.prev = cur // 反向链接,因为有tail,不需要判断
this.len++
}
addAtTail (val) {
const tail = this.operate(this.len + 1) // 指向尾部‘tail'节点
const cur = new ListNode(val, tail, tail.prev) // 建立尾部节点
tail.prev = cur // 反向链接
tail.prev.prev.next = cur // 正向链接
this.len++
}
addAtIndex (index, val) {
if (index < 0 || index > this.len) { //不在范围之内,包括this.len,因为有可能在尾部添加
return -1
}
if (index === 0) return this.addAtHead(val)
if (index === this.len) return this.addAtTail(val)
const operateNode = this.operate(index)
const cur = new ListNode(val, operateNode.next, operateNode)
operateNode.next = cur
operateNode.next.next.prev = cur
this.len++
}
deleteAtIndex (index) {
if (index < 0 || index >= this.len) {
return -1
}
const operateNode = this.operate(index)
operateNode.next = operateNode.next.next
operateNode.next.prev = operateNode
this.len--
}
}
C++代码
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
void addAtIndex(int index, int val) {
if (index > _size) {
return;
}
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};