双链表
定义
双链表是链表的一种,它的每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。这样使得链表可以双向遍历。
运用情况
- 频繁进行前后双向遍历操作时非常有用,比如在一些需要来回移动处理数据的场景。
- 可以方便地实现诸如栈、队列等数据结构的混合操作。
- 在一些需要快速插入和删除节点,同时又需要双向遍历的特定算法和程序中经常被采用。
注意事项
- 内存管理要恰当,确保正确地分配和释放节点内存,避免内存泄漏或非法访问。
- 对节点指针的操作要格外小心,防止出现悬空指针或错误指向。
- 在进行插入、删除等操作时,要注意前后节点指针的正确更新,保持链表的完整性。
解题思路
当使用双链表解决问题时,一般思路如下:
- 明确问题中涉及到的操作,比如插入、删除、遍历等。
- 根据操作确定如何更新节点的前后指针以维持链表结构。
- 在进行复杂操作时,仔细考虑边界情况和特殊情况,确保算法的正确性和鲁棒性。
- 可以借助一些辅助指针或变量来方便地进行节点的定位和操作。例如,在寻找特定节点时,可以利用前后向的遍历。
- 对于一些需要高效操作的场景,优化指针的操作顺序和算法流程以提高性能。
AcWing.827双链表
题目描述
827. 双链表 - AcWing题库
运行代码
#include <iostream>
using namespace std;
const int N = 100010;
int l[N], r[N], idx, e[N];
int n;
void init()
{
r[0] = 1, l[1] = 0, idx = 2;
}
void insert(int k, int x)
{
e[idx] = x;
l[idx] = k, r[idx] = r[k];
l[r[k]] = idx, r[k] = idx ++ ;
}
void Remove(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main()
{
init();
cin >> n;
while(n -- )
{
string op;
int k, x;
cin >> op;
if(op == "L")
{
cin >> x;
insert(0, x);
}
else if(op == "R")
{
cin >> x;
insert(l[1], x);
}
else if(op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else if(op == "IR")
{
cin >> k >> x;
insert(k + 1, x);
}
else
{
cin >> k;
Remove(k + 1);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
代码思路
- 初始化:
- 使用两个数组
l
和r
来分别存储每个节点的左指针和右指针。 - 数组
e
用于存储每个节点的值。 idx
用于追踪下一个要插入的节点的索引。init()
函数初始化双链表,将第一个虚拟节点(通常用作头节点)的右指针指向第二个虚拟节点(通常用作尾节点的前一个节点),并将第二个虚拟节点的左指针指向第一个虚拟节点。
- 使用两个数组
- 插入操作:
insert(int k, int x)
函数用于在给定节点k
的右侧插入一个新节点,其值为x
。- 首先,将新节点的值存储在
e[idx]
中。 - 然后,更新
l[idx]
和r[idx]
以指向正确的邻居节点。 - 同时,更新
k
的右邻居和idx
的左邻居的指针,使其指向新节点。 - 最后,递增
idx
以准备下一次插入。
- 删除操作:
Remove(int k)
函数用于删除给定节点k
(注意:这里的k
是节点在数组中的索引,而不是节点值)。- 通过更新
k
的左邻居的右指针和k
的右邻居的左指针,来删除节点k
。 - 注意:这里没有显式地释放或重置数组
e
中的值,因为C++的数组不会自动管理内存。但由于e
是静态分配的,它将在程序结束时自动被销毁。
- 主函数:
- 读取一个整数
n
,表示将要进行的操作数。 - 对于每个操作,读取一个操作码
op
和一个或两个参数(取决于操作)。 - 根据操作码执行相应的操作:
"L"
: 在双链表的左侧插入一个新节点。"R"
: 在双链表的右侧插入一个新节点。"IL"
: 在第k
个节点的左侧插入一个新节点。"IR"
: 在第k
个节点的右侧插入一个新节点。- 其他: 删除第
k
个节点。
- 在所有操作完成后,遍历并打印双链表中的节点值。
- 读取一个整数
注意:
- 这里的节点索引是从虚拟头节点(索引为0)和虚拟尾节点的前一个节点(索引为1)开始的。因此,当插入或删除节点时,需要使用
k + 1
来引用实际的节点索引(从2开始)。 - 代码没有包含错误检查,例如检查
k
是否超出了链表的合法范围。在实际应用中,应添加这些检查以防止程序崩溃或产生不可预测的行为。
改进思路
- 添加错误处理:
- 在进行插入或删除操作时,检查给定的索引
k
是否在链表的有效范围内。 - 检查操作是否符合预期,比如尝试在已经不存在的节点上进行操作。
- 在进行插入或删除操作时,检查给定的索引
- 使用结构体或类:
- 创建一个结构体或类来表示链表节点,这样可以使代码更加清晰,并减少错误的可能性。
- 在这个结构体或类中,可以包含节点的值、左指针和右指针。
- 封装链表操作:
- 将链表的操作(如插入、删除、遍历等)封装到类的方法中,这样可以使代码更加模块化。
- 封装后的代码可以提供更好的封装性、继承性和多态性。
- 使用迭代器或指针:
- 考虑使用迭代器或指针来遍历链表,而不是直接使用数组索引。这可以使代码更加灵活,并减少错误的可能性。
- 改进输入验证:
- 在
main
函数中,添加对输入数据的验证,确保输入的操作码和参数是有效的。 - 可以使用
std::cin.fail()
或类似的函数来检查输入是否成功。
- 在
- 使用标准库容器:
- 如果可能的话,考虑使用C++标准库中的容器(如
std::list
)来实现链表。这样可以减少手动管理内存和指针的复杂性,并提高代码的可读性和可维护性。
- 如果可能的话,考虑使用C++标准库中的容器(如
- 改进遍历打印逻辑:
- 遍历链表时,可以使用循环而不是递归,以提高效率。
- 在打印链表时,可以添加一些分隔符或换行符,使输出更加清晰。
- 优化内存使用:
- 如果链表中的节点数量非常大,可以考虑使用动态内存分配来减少内存使用。
- 但是,请注意,动态内存分配也会增加内存泄漏和指针错误的风险。
- 添加注释和文档:为代码添加注释,解释每个函数、类和变量的作用。编写文档,描述如何使用这个链表类,以及它的特性和限制。
- 使用异常处理:如果在链表操作中发生错误(如无效索引、无效操作等),可以使用C++的异常处理机制来抛出异常,并在调用者处捕获和处理这些异常。这可以使错误处理更加清晰和一致。