算法 {双链表(单链表)}
@LOC: 2;
双链表(单链表)
定义
由若干节点组成, 每个节点拥有 {id
: 唯一性标识, previous_id
: 前驱节点的id号, next_id
: 后驱节点的id号};
头节点: 从头节点开始, 不断通过next_id
, 可以遍历完整个链表;
尾节点: 从尾节点开始, 不断通过previous_id
, 可以遍历完整个链表;
性质
# {链表的连通性, 删除节点的2种方式}; MAKR: @LOC_1
;
从算法角度 (或数据结构角度), 链表结构里 只能有1条链表, 即H->...->T, H1->...->T1
这是错误的 因为有2条链表;
即链表 一定是连通的, 为什么要谈这个问题呢?
.
因为如果你只有插入操作 那么这个链表一定始终是连通的, 但是一旦有删除操作 那么你要如何定义这个删除操作?
假如你定义的删除操作是: H->...->p->c->n->...->T
, 删除c
节点后 变成了H->...->p
和n->...->T
, 那么原来的链表 就不再连通了, 就变成2条链表了;
.
那么, 根据上面讲的链表的连通性, 这就不符合了, 因此 这种删除方式, 在链表了 是不被允许的;
因此, 你要做到: 既删除节点, 还要继续保持链表的连通性; 要满足这两点, 有2种删除策略:
–
## 删除节点–方式1;
原链表: H->...->p->c->n->...->T
, 删除c
后 变成: H->...->p->n->...->T
;
这个经典做法 也是链表默认的删除节点方法, 即将要删除节点的前驱和后驱 连接起来;
.
如果你认为这种方式 头尾节点不会改变, 这是错误的; 因为如果c = H
(或c = T
), 那么头尾节点是会改变的; (如果c
不是{头/尾}节点, 那么头尾节点就不会改变);
–
## 删除节点–方式2;
原链表: H->...->p->c->n->...->T
, 删除c
后 变成: n->...->T->H->...->p
;
这种删除策略, 即如果c = {H/T}
则和方式1一样 直接删除该点即可;
但当c != {H, T}
时 此时他的做法是: [删除当前点, 连接T->H
], 然后头尾节点一定会改变 (这和方式1不同, 方式1的头尾节点 在这种情况下 是不会改变的), 新的头是后驱n
而新的尾是前驱p
;
.
注意, 连接T->H
和 连接H->T
是完全不同的, 只有连接T->H
(即next[ T] = H, previous[ H] = T
) 才能保证链表是连通的, 否则如果是H->T
那这个链表是无效的 就不是链表了;
例题: LINK: @LOC_0
;
@DELIMITER
# 节点的id号;
每个节点 有3部分组成: {id号, previous_id, next_id
前驱后驱节点id号, data数据} (比如保证所有节点的id号都不同, 因为id号是唯一标识一个节点的依据), 很重要的是 这id号 不是用户来指定设定的, 而是链表内部自己维护的;
.
假如让用户来指定id号, 对于插入删除操作 就无法做到O(1)时间了; 比如用户指定的id号为-123
, 那么根据这个id号 要想得到他的节点, 只能通过O(n)的依次遍历去找这个id号;
.
因此, 必须由系统自己来设定这个id号, 具体做法是: 所有节点都是在一个数组Arr
里, 每次往链表里插入一个节点, 这个节点 其实就是数组里的一个元素 比如是Arr[ x]
, 那么x
就是这个节点的id号;
这也有点矛盾, 因为一切操作(插入,删除) 这肯定是用户调用的, 他们都需要依据id号来进行操作 (比如删除id=x
的节点, 在id=x
的节点的后面 插入一个节点);
.
可是, 这id号 是系统内部自己指定的, 用户怎么知道呢?
.
这其实并不矛盾, 2点可以说明:
.
第一, 每个插入操作的函数 都会返回一个id
号 表示新插入这个节点的id号;
.
第二, 其实id号 你自己也可以计算, 系统指定id号的规则 就是有一个id_counter
, 根据你插入过多少节点, 比如你插入过x
个节点 (即调用了x
次有关插入节点的函数), 那么 如果你再插入一个节点 他的id号就会是x
; (注意, 插入过的节点数 不等于 当前链表里节点数, 因为可能会有删除过节点; 即新插入的节点的id号 不仅和当前链表里的节点的id不同 而且和已经删除的节点的id也不同);
.
假如你就是要自己去指定id号, 那么将你的id号 和 你插入的这个节点的系统id号, 你自己去做一个关联;
.
.
一般来说, 题目就是通过第几次插入的节点 作为id号, 比如题目说 删除第x次插入的节点 (看到这个题目, 基本就是链表的题目), 那么第x次插入的节点 他的id号 就是
x
−
1
x-1
x−1;
@DELIMITER
# 数组与链表;
我们可能会拿数组来和链表做类比, 认为链表是一种特殊的数组, 其实这是完全错误的;
.
因为链表就是从头节点到尾节点 比如H->a->b->T
, 他对应到数组 也就是: [H, a, b, T]
, 那么 比如对于插入操作 插入c
, 链表会变成H->a->c->b->T
对应数组是[H, a, c, b, T]
, 看似好像是合理的 其实这是错误的;
.
.
错误原因: 不管数组还是链表, 都必须有一个id
来标识元素, 数组是通过下标 而链表 他节点里面会有一个id
号 (所有节点的id号 一定是独一无二的); 此时再对比数组和链表, 插入前后 H,a,b,T
这些节点 他的id号肯定是相同的, 但是 如果对应到数组 原来b,T
的下标是2,3
, 可是 插入之后 他俩的下标变成了3,4
;
.
.
因此 用数组来模拟链表 是不正确的, 最本质的差异是 链表是通过让每个节点拥有previous_id, next_id
指向前驱后驱节点 来确定节点的相对位置, 而数组是通过固定的线性下标 因此数组的元素的相对位置 就是绝对位置 是固定死了的; 当然 他俩还有很多其他不同 比如插入删除的时间度问题;
@DELIMITER
# 方向是唯一的;
不管是单链表 还是双链表 (其实他俩本质上是一样的), 单链表就是没有previous_id
的双链表;
.
单链表肯定是有方向的 这很容易判断, 因为每个节点 都只能往一个方向走 即走next_id
这个方向;
.
.
但你容易以为 双链表是没有方向的, 因为每个节点 可以往2个方向走 即previous_id, next_id
都可以走; 但严格讲 这种说法是错误的, 虽然每个节点有2个方向 但对于这个链表 他的方向 是独一无二的, 即next_id
是他的方向;
.
.
.
之所以再加一个previous_id
其实只是一个附加功能 记录下前驱节点;
.
不管{单/双}链表 他都有唯一的head_id, tail_id
头和尾节点, 从head_id
通过next_id
遍历到tail_id
这个方向 就是链表的方向; 也就是, 头和尾 2个节点, 就确定了链表的方向;
.
因此, 给定链表里的两个邻接点a<->b
, 虽然从图的角度看 好像这是一条无向边 无法说谁指向谁, 但这是错误的, 他一定是有方向的;
.
.
如果是next_id[ a] = b
则说明方向是a->b
, 否则一定是next_id[ b] = a
那么方向是b->a
; 明白这一点, 对理解双链表非常非常重要, 虽然是双向, 但其实他是有方向的! 而是方向是唯一的;
.
.
这里又引入一个概念, 即连接2个节点操作, 比如给定2个节点a,b
你要将他俩连接起来, 那么 你必须要指定方向, 即是a->b
还是 b->a
;
.
.
千万不要双链表的边 理解为是无向边 这是错误的; 如果是a->b
那么对应的是next[ a] = b, previous[ b] = a
; 而如果是b->a
则对应的是next[ b] = a, previous[ a] = b
;
.
.
也就是, next
是唯一用来表明方向的; 所以不能看做是无向边 因为一个是next
另一个是previous
, 并不是说 next[ a] = b, next[ b] = a
不是这样的;
假如双链表没有方向, 那么是无法进行插入操作 的, 下面介绍下插入操作;
## 插入操作;
对于链表H-a-T
, 插入操作有几种情况:
.
?-H-a-T
(在链表的头部插入);
.
H-?-a-T
(在a
节点的前面插入);
.
H-a-?-T
(在a
节点的后面插入);
.
H-a-T-?
(在链表的尾部插入);
即可以总结为2种操作: {操作1: 在链表的头部/尾部插入, 操作2: 在x
节点的前面/后面插入};
.
你可能认为: 操作1 可以归纳到 操作2里面 (比如在链表头部插入, 不就是在head
节点的前面插入嘛), 这是不对的; 当链表为空时, 此时没有节点 (即x
节点是不存在的), 那么操作2此时就不可行了, 但是 操作1是可行的 (即当链表为空时, 通过操作1来插入);
@DELIMITER
模板
//{ `LinkedList`--声明;
template< class _Type_Data>
class __LinkedList{
public:
struct Node_{
public:
int Previous_id, Next_id;
_Type_Data Data;
};
//--
const Node_ * Nodes;
//--
__LinkedList( int);
void Initialize();
int Add_head( const _Type_Data &);
int Add_tail( const _Type_Data &);
int Add_back_byId( int, const _Type_Data &);
int Add_front_byId( int, const _Type_Data &);
void Delete_type1( int);
void Delete_type2( int);
void Modify( int, const _Type_Data &);
bool Exist( int);
//--
int Get_nodesCount() const{ return nodes_count;}
int Get_headId() const{ return head_id;}
int Get_tailId() const{ return tail_id;}
private:
int lengthMaximum;
Node_ * nodes;
int head_id, tail_id;
int id_counter;
int nodes_count;
bool * id_exist;
};
//} `LinkedList`--声明;
//{ `LinkedList`--实现;
//<
// @BRIFE
// . 虽然是双链表 但他是有方向的 而且方向是唯一的 即从`head_id->...->tail_id`;
// . 链表一定是连通的, 即从任意节点开始遍历 一定可以遍历到所有*未删除的节点(即`is_exist=true`)*;
// @NAME( id_counter): 因为最初链表一定为空 你一定需要进行添加节点操作, 每个添加过的节点 都有一个独一无二的`id`号;
// . 注意是`添加过的` 因为节点可能被删除, 所有节点 不管是当前链表里的 还是已经被删除了的, 他们的`id`号 都是不同的;
// . 其实这个`id`号 就对应该节点的`插入时序` (即 调用`Add_?`函数的次序), 比如最先第一个插入的节点 id号一定为`0`;
// . `id = 5`的节点 说明是他第`6`个插入进来的;
// @NAME( nodes): `nodes[ x]`这个节点, 他里面是不存储`id`号的 (链表的节点由3部分组成: {id号, 前驱后驱节点的id号, 数据})
// . 这里不存储id号, 因为数组下标`x` 就是他的id号, 再存储就重复了;
template< class _Type_Data> __LinkedList< _Type_Data>::__LinkedList( int _lengthMaximum)
:
lengthMaximum( _lengthMaximum){
nodes = new Node_[ lengthMaximum]; id_exist = new bool[ lengthMaximum];
//--
Nodes = nodes;
//--
Initialize();
}
template< class _Type_Data> void __LinkedList< _Type_Data>::Initialize(){
head_id = tail_id = -1; id_counter = 0; nodes_count = 0;
memset( id_exist, false, sizeof( lengthMaximum));
}
template< class _Type_Data> int __LinkedList< _Type_Data>::Add_head( const _Type_Data & _data){
//< 在链表头插入; 返回值为*新插入的这个节点的id号*;
ASSERT_( id_counter < lengthMaximum);
//--
nodes[ id_counter].Data = _data; id_exist[ id_counter] = true;
//--
nodes[ id_counter].Previous_id = -1; nodes[ id_counter].Next_id = head_id;
//--
if( head_id != -1) nodes[ head_id].Previous_id = id_counter;
//--
head_id = id_counter;
if( tail_id == -1) tail_id = id_counter;
//--
++ nodes_count; ++ id_counter;
//--
return id_counter - 1;
}
template< class _Type_Data> int __LinkedList< _Type_Data>::Add_tail( const _Type_Data & _data){
//< 在链表尾部插入; 返回值为*新插入的这个节点的id号*;
ASSERT_( id_counter < lengthMaximum);
//--
nodes[ id_counter].Data = _data; id_exist[ id_counter] = true;
//--
nodes[ id_counter].Previous_id = tail_id; nodes[ id_counter].Next_id = -1;
//--
if( tail_id != -1) nodes[ tail_id].Next_id = id_counter;
//--
tail_id = id_counter;
if( head_id == -1) head_id = id_counter;
//--
++ nodes_count; ++ id_counter;
//--
return id_counter - 1;
}
template< class _Type_Data> int __LinkedList< _Type_Data>::Add_back_byId( int _id, const _Type_Data & _data){
//< 在`id`号节点的`后面` 插入节点; 返回值为*新插入的这个节点的id号*;
ASSERT_( _id >= 0 && _id < lengthMaximum && id_exist[ _id] && id_counter < lengthMaximum);
//--
nodes[ id_counter].Data = _data; id_exist[ id_counter] = true;
//--
nodes[ id_counter].Previous_id = _id; nodes[ id_counter].Next_id = nodes[ _id].Next_id;
//--
if( nodes[ _id].Next_id != -1) nodes[ nodes[ _id].Next_id].Previous_id = id_counter;
nodes[ _id].Next_id = id_counter;
//--
if( _id == tail_id) tail_id = id_counter;
//--
++ nodes_count; ++ id_counter;
//--
return id_counter - 1;
}
template< class _Type_Data> int __LinkedList< _Type_Data>::Add_front_byId( int _id, const _Type_Data & _data){
//< 在`id`号节点的`前面` 插入节点; 返回值为*新插入的这个节点的id号*;
ASSERT_( _id >= 0 && _id < lengthMaximum && id_exist[ _id] && id_counter < lengthMaximum);
//--
nodes[ id_counter].Data = _data; id_exist[ id_counter] = true;
//--
nodes[ id_counter].Previous_id = nodes[ _id].Previous_id; nodes[ id_counter].Next_id = _id;
//--
if( nodes[ _id].Previous_id != -1) nodes[ nodes[ _id].Previous_id].Next_id = id_counter;
nodes[ _id].Previous_id = id_counter;
//--
if( _id == head_id) head_id = id_counter;
//--
++ nodes_count; ++ id_counter;
//--
return id_counter - 1;
}
template< class _Type_Data> void __LinkedList< _Type_Data>::Delete_type1( int _id){
//< 删除`id`号 (方式1); 假如当前链表为`head->...->p->id->n->...->tail` 则删除后变成`head->...->p->n->...->tail`;
ASSERT_( _id >= 0 && _id < lengthMaximum && id_exist[ _id]);
//--
id_exist[ _id] = false; -- nodes_count;
//--
if( _id == head_id){
if( nodes[ _id].Next_id == -1) head_id = tail_id = -1;
else nodes[ nodes[ _id].Next_id].Previous_id = -1, head_id = nodes[ _id].Next_id, nodes[ _id].Next_id = -1;
}
else if( _id == tail_id){
if( nodes[ _id].Previous_id == -1) head_id = tail_id = -1;
else nodes[ nodes[ _id].Previous_id].Next_id = -1, tail_id = nodes[ _id].Previous_id, nodes[ _id].Previous_id = -1;
}
else{
nodes[ nodes[ _id].Previous_id].Next_id = nodes[ _id].Next_id;
nodes[ nodes[ _id].Next_id].Previous_id = nodes[ _id].Previous_id;
nodes[ _id].Previous_id = nodes[ _id].Next_id = -1;
}
}
template< class _Type_Data> void __LinkedList< _Type_Data>::Delete_type2( int _id){
//< 删除`id`号 (方式2); 假如当前链表为`head->...->p->id->n->...->tail` 则删除后变成`n->...->tail->head->...->p`;
ASSERT_( _id >= 0 && _id < lengthMaximum && id_exist[ _id]);
//--
id_exist[ _id] = false; -- nodes_count;
//--
if( _id == head_id){
if( nodes[ _id].Next_id == -1) head_id = tail_id = -1;
else nodes[ nodes[ _id].Next_id].Previous_id = -1, head_id = nodes[ _id].Next_id, nodes[ _id].Next_id = -1;
}
else if( _id == tail_id){
if( nodes[ _id].Previous_id == -1) head_id = tail_id = -1;
else nodes[ nodes[ _id].Previous_id].Next_id = -1, tail_id = nodes[ _id].Previous_id, nodes[ _id].Previous_id = -1;
}
else{
nodes[ tail_id].Next_id = head_id; nodes[ head_id].Previous_id = tail_id;
head_id = nodes[ _id].Next_id, tail_id = nodes[ _id].Previous_id;
nodes[ _id].Previous_id = nodes[ _id].Next_id = -1;
}
}
template< class _Type_Data> void __LinkedList< _Type_Data>::Modify( int _id, const _Type_Data & _data){
//< 修改节点;
ASSERT_( _id >= 0 && _id < lengthMaximum && id_exist[ _id]);
//--
nodes[ _id].Data = _data;
}
template< class _Type_Data> bool __LinkedList< _Type_Data>::Exist( int _id){
//< 判断`id`号节点 是否存在 (即是否已经被删除了);
ASSERT_( _id >= 0 && _id < lengthMaximum);
//--
return id_exist[ _id];
}
//} `LinkedList`--实现;
例题
双链表模板: https://www.acwing.com/problem/content/829/
@DELIMITER
单链表模板: https://www.acwing.com/problem/content/828/
@DELIMITER
MARK: @LOC_0
;
删除节点-方式2 的应用;
LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=130464894
;
@TODO
{ ___List_
//template< class _T_> struct ___List_{
//struct __Node_{
// __Node_ * Prev, * Next;
// _T_ Data;
//};
// __Node_ * __Head; // 虽然是*双向链表* 即`f-...-b`(注意`f-b`也是有边的) 但我们还是从一维的角度去处理他 即他也是有头的 `f`就是头 `f.Next`就是尾;
// int Size;
// void Initialize(){
// __Head = nullptr;
// Size = 0;
// }
// __Node_ * Insert_Front( __Node_ * _ind, _T_ const& _data){ // `ind.Prev = new Node`;
// ++ Size;
// if( _ind == nullptr){
// ASSERT_(__Head == nullptr);
// __Head = new __Node_;
// __Head->Prev = __Head->Next = __Head;
// __Head->Data = _data;
// return;
// }
// __Node_ * cur = new __Node_;
// cur->Data = _data;
// cur->Prev = _ind->Prev;
// cur->Next = _ind;
// _ind->Prev = cur;
// if( _ind == this->__Head){ this->__Head = cur;}
// return cur;
// }
//friend std::ostream& operator<<( std::ostream & _cout, ___List_ const& _a){
// _cout<< "___List_-Debug-Begin\n";
// _cout<< "Size: "<< _a.Size<< "\n";
// if( _a.__Head != nullptr){
// ASSERT_( _a.Size==0, _a.Size);
// _cout<< "[";
// _cout<< _a.__Head->Data;
// _cout<< "]";
// }
// _cout<< "___List_-Debug-End\n";
// return _cout;
//}
//};