链表知识点总结(一):链表的常见操作以及复杂度分析

前言

链表是数据结构中最基础的链式结构,也是后面构成图、树的基础。为此,我觉得有必要专门开几篇文章写写链表相关的内容,但是如果从零开始写起太过于枯燥,文章也会变得冗长,所以本文只写一些总结性的内容,对其中的原理不深究。

 

另外,本文默认使用节点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)

 

 

总结

单链表的各种操作都不难理解,对初学者而言,需要多理解各种操作的实现原理,必要时在草稿纸上画草图梳理下思路,在理解各种操作的原理后最好动手打几遍单链表的完整实现,这样才能真正掌握学到的知识点。总之,多看,多想,多写。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章