链表是数据结构中最基础的链式结构,也是后面构成图、树的基础。为此,我觉得有必要专门开几篇文章写写链表相关的内容,但是如果从零开始写起太过于枯燥,文章也会变得冗长,所以本文只写一些总结性的内容,对其中的原理不深究。
另外,本文默认使用节点Node的C++定义为:
class Node
{
public:
int data;
Node *next;
Node(int dd = -999, Node *nn = NULL) : data(dd), next(nn) {}
};
下文中出现的checkIndex函数为检查索引参数是否合法的函数,本来是用于我的封装链表中的(封装的链表中还有一个size成员,可以知道链表的长度,以此来判断索引是否合法),具体实现这里就不给出了,这里默认传入的参数都是合法的、且链表为非空链表,忽略这个函数调用即可。
下面总结一些我个人的链表常见操作的实现,并且会带上简单的复杂度分析。以下实现默认是对带头节点的单链表进行的操作。
void delLL(Node *header)
{
Node *pre = NULL;
Node *cur = header;
while (cur != NULL)
{
pre = cur;
cur = cur->next;
delete pre;
}
}
以一前一后两个指针pre和cur扫链表,不断让两个指针后移,同时删除pre所指的节点,直到cur指向NULL为止。凡是需要用两个指针扫链表的操作都建议根据实际用途命名为pre 和 cur,代表前一个节点和当前指向节点。时间复杂度为O(n),由于需要对链表进行遍历,所以是On。
void add(Node* header, int data, int index)
{
checkIndex(index);//判断一下索引是否合法
Node *pre = header;
for (int i = 0; i < index-1; i++)
{
pre = pre->next;
}
pre->next = new Node(data, pre->next);
}
这里我默认传进来的index为从1开始的索引,所以for的中止条件需要-1,实现思路是用pre指针扫链表,先找到要插入位置的前一个个节点,然后再让新节点成为pre指向节点的下一个节点,注意,此时pre后面可能还有节点,要把pre节点的next赋值给新节点的next,就把要插入的节点插入进去了,这里使用了Node类的一个构造函数,让代码变得简洁。
另外,给链表添加头节点的好处也在这里体现,试想一下,如果没有头节点的话,那么add函数就得判断一下当前要插入的位置是否是第一个位置,是的话就得修改header指针,不是的话就进入上面add函数里的循环,去找待插入位置的前一个节点。但是我们给链表加入头结点后,不管插入的是不是第一个位置,我们都不需要修改header指针,也就是不需要额外的if来判断,这样就简化了代码。
时间复杂度为O(n),从头部插入的话是O1的复杂度,但是插入到其他地方的话就就是On,所以不管是从最坏的角度考虑,还是从综合的角度考虑,其时间复杂度都为On。
上面讲的是在任意位置的插入,其实就是教科书里说的尾插法,下面说说头插法。
头插法,顾名思义,就是在链表的头部插入一个元素,如果对一个链表一直使用头插法插入元素的话,将会得到跟插入顺序倒序的链表。代码:
void HeadInsertion(Node *header, int data)
{
Node *tmp = new Node;
tmp->data = data;
tmp->next = header->next;
header->next = tmp;
}
如果是按照我上面节点定义的那样定义了构造函数的话,其实可以这样写:
void HeadInsertion(Node *header, int data)
{
header->next = new Node(data,header->next);
}
对头插法来说,时间复杂度是O(1)的,因为它不需要遍历整个链表。
int remove(Node* header, int index)
{
checkIndex(index);//保证索引合法
Node *pre = header;
for (int i = 0; i < index-1; i++)
{
pre = pre->next;
}
Node *tmp = pre->next;
pre->next = tmp->next;
int res = tmp->data;
delete tmp;
return res;
}
这里也是跟插入一样,先找到要删除节点的上一个节点,因为要返回这个要删除节点的值,所以得先用临时变量res把data存起来后面返回,又因为要把这个要删除节点的内存释放,所以还得用一个临时指针tmp保存这个要删除节点。时间复杂度为O(n),分析同单链表的插入。
void print(Node* header)
{
Node *cur = header->next;
while (cur != NULL)
{
cout << cur->data << " ";
cur = cur->next;
}
cout << endl;
}
这里以输出整个链表为例,用cur指针扫一遍链表即可,要注意,cur初始值应为第一个数据节点,而不是头节点。时间复杂度O(n),遍历嘛,肯定是和链表的长度n有关。
void reverse(Node *header)
{
Node *cur = header->next;
Node *tmp = NULL;
while (cur != NULL)
{
tmp = cur->next;
cur->next = (header->next == cur ? NULL : header->next);//将反转后最后一个节点的next置空,其实就是反转前的第一个数据节点
header->next = cur;
cur = tmp;
}
}
这里其实就是从第一个数据节点开始,将节点逐个用头插法插入到原链表中(头节点),就完成了反转操作,显然,这个操作的时间复杂度也是O(n)的
单链表的各种操作都不难理解,对初学者而言,需要多理解各种操作的实现原理,必要时在草稿纸上画草图梳理下思路,在理解各种操作的原理后最好动手打几遍单链表的完整实现,这样才能真正掌握学到的知识点。总之,多看,多想,多写。