哦,天哪,我们进入了 C++“链表教程”,上次你问我时我说它是不切实际的。
但也许我有时间写它。
我会先给自己泡点茶,然后分小批发布,一次一个。
基本单链表。
一个基本的单链表由节点组成,其中每个节点都包含一个指向列表中下一个节点的指针,例如称为next
。按照惯例next
==0 表示列表的结尾。所以,让我们创建这样一个简单的列表,遍历它(这里只是显示内容),并通过释放节点进行清理:
#include <iostream>
using namespace std;
struct node_t
{
node_t* next;
int value;
node_t( int const v = 0 )
: next( 0 ), value( v )
{}
};
int main()
{
char const blah[] = "moonflower";
node_t* first = 0;
// Create the list.
for( auto ch : blah )
{
node_t* node = new node_t( ch );
node->next = first; // After this `node` points to the new first node.
first = node; // Now `first` also points to the new first node.
}
// Display it
for( node_t* p = first; p != 0; p = p->next )
{
cout << p->value << " ";
}
cout << endl;
// Delete the list
while( first != 0 )
{
node_t* const doomed = first;
first = first->next;
delete doomed;
}
}
对于最后的清理,重要的是按正确的顺序做事,以免访问已删除的节点。访问已删除的节点可能会起作用。但是,由于墨菲定律,它会在以后失败,在事情应该起作用的绝对最关键的时间点上。
输出:
0 114 101 119 111 108 102 110 111 111 109
这些是 ASCII 字符代码,包括字符串的终止零。
注意数字的顺序!:-)
插入单链表。
与直接使用数组相比,链表的主要优点是插入新节点需要恒定的时间。在直接使用数组的情况下,插入新项必须将所有项移动到它之后(或之前),这需要与数组大小成正比的时间。但是,请注意(1)查找插入点的时间通常被强制为线性列表大小,而它可以是数组的对数,以及(2)通过使用称为“插入间隙”或“光标间隙”的技术” 对于数组来说,插入一个项目也可以是恒定的时间(以一些轻微的成本)。
为了在升序位置插入一个数字,对于基本列表,您需要一个指向插入点之前节点的指针,以便next
可以更新该节点以指向新节点。
由于基本列表在第一个节点之前没有这样的节点,因此在列表开头插入成为一种丑陋的特殊情况,如下所示:
#include <iostream>
using namespace std;
struct node_t
{
node_t* next;
int value;
node_t( int const v = 0 )
: next( 0 ), value( v )
{}
};
int main()
{
char const blah[] = "moonflower";
node_t* first = 0;
// Create the list.
for( auto ch : blah )
{
node_t* node = new node_t( ch );
// Find the insertion point.
node_t* p = first;
node_t* p_node_before = 0;
while( p != 0 && p->value < ch )
{
p_node_before = p;
p = p->next;
}
if( p_node_before == 0 )
{
// Insert at start of list, just like in the first program.
node->next = first; // After this `node` points to the new first node.
first = node; // Now `first` also points to the new first node.
}
else
{
// Insert within the list or at the end of the list.
node->next = p_node_before->next;
p_node_before->next = node;
}
}
// Display it
for( node_t* p = first; p != 0; p = p->next )
{
cout << p->value << " ";
}
cout << endl;
// Delete the list
while( first != 0 )
{
node_t* const doomed = first;
first = first->next;
delete doomed;
}
}
输出:
0 101 102 108 109 110 111 111 111 114 119
请注意,由于搜索插入位置,每次插入平均花费的时间与列表的最终长度成正比,因此总的来说这是二次时间,O(n 2 )。这是一个简单的插入排序。我希望我没有引入任何错误!:-)
巧妙的指针对指针。
减少基本列表开头插入混乱的一种方法是注意您实际上只需要访问next
插入点之前的节点字段。
您不需要访问插入点之前的完整节点。
所以你可以只用一个指向指针的next
指针。即指向指针的指针。对于列表的开头,first
指针变量可以很好地服务,就好像它是next
之前某个节点中的字段一样,所以最初指针指向指针可以只指向first
指针(嗯),然后它可以移动在next
指针上,像这样:
#include <iostream>
using namespace std;
struct node_t
{
node_t* next;
int value;
node_t( int const v = 0 )
: next( 0 ), value( v )
{}
};
int main()
{
char const blah[] = "moonflower";
node_t* first = 0;
// Create the list.
for( auto ch : blah )
{
node_t* node = new node_t( ch );
// Find the insertion point.
node_t** p_next_before = &first;
while( *p_next_before != 0 )
{
node_t* const next_node = *p_next_before;
if( next_node->value >= ch )
{
break;
}
p_next_before = &next_node->next;
}
// Insert at start of list, just like in the first program.
node->next = *p_next_before; // After this `node` points to the new first node.
*p_next_before = node; // Now ... also points to the new first node.
}
// Display it
for( node_t* p = first; p != 0; p = p->next )
{
cout << p->value << " ";
}
cout << endl;
// Delete the list
while( first != 0 )
{
node_t* const doomed = first;
first = first->next;
delete doomed;
}
}
输出还是
0 101 102 108 109 110 111 111 111 114 119
应该是这样。
起初,我直接在while
循环头中编写了带有相当长的延续条件的代码。但是遵循应该命名几乎所有东西的规则,我将其中的一部分移到循环内,然后加上一个break
语句。所以这就是声明的原因break
:支持名称next_node
。
在 C++ 中,指向指针的指针有时表示为指向指针的引用。例如,从基本列表中取消链接节点的函数可能采用引用指针参数,而不是 C 风格的指针指针。我发现它更干净,当它适用时。
使用头节点。
指针对指针的替代方法是在第一个实节点之前引入一个虚拟节点。那么即使是一个空列表也有一个物理节点,即虚拟节点。虚拟节点通常称为头节点。
在编译器完成对机器代码的转换和优化之后,使用头节点的代码可能与使用指针到指针技术的代码相同,只是头节点涉及更多的分配,并且需要更多的时间房间比单个指针变量。但不同之处在于人们如何看待代码,如何理解它。从概念上讲,拥有头节点与使用指针对指针技术非常不同——例如,如果您绘制列表结构,那么带有头节点的列表看起来与没有此类节点的列表明显不同。
无论如何,代码:
#include <iostream>
using namespace std;
struct node_t
{
node_t* next;
int value;
node_t( int const v = 0 )
: next( 0 ), value( v )
{}
};
int main()
{
char const blah[] = "moonflower";
node_t* header_node = new node_t();
// Create the list.
for( auto ch : blah )
{
node_t* node = new node_t( ch );
// Find the insertion point.
node_t* p_before = header_node;
while( p_before->next != 0 && p_before->next->value < ch )
{
p_before = p_before->next;
}
// Insert at start of list, just like in the first program.
node->next = p_before->next; // After this `node` points to the new first node.
p_before->next = node; // Now ... also points to the new first node.
}
// Display it
for( node_t* p = header_node->next; p != 0; p = p->next )
{
cout << p->value << " ";
}
cout << endl;
// Delete the list
auto& first = header_node; // Just a renaming for clarity.
while( first != 0 )
{
node_t* const doomed = first;
first = first->next;
delete doomed;
}
}
和以前一样,输出仍然是
0 101 102 108 109 110 111 111 111 114 119
应该是这样。