0x00000003 2.数据结构和算法 基础数据结构 链表

文章目录

  • 基础知识简单总结
    • 单向链表
    • 双向链表
    • 拓展与补充:
  • C++11常见API
    • list
    • forward_list
  • 典型试题
    • 单一链表场景
      • 链表翻转
      • 环形链表
      • 查找和删除链表节点
      • 链表排序
    • 多个链表场景
      • 链表相交
      • 合并和分隔链表
      • 链表的运算
    • 其他题型
  • References:


本部分知识比较简单。算法题比较基础,并且比较注重代码细节。注意:尽可能的一题多解。

基础知识简单总结

单向链表

  1. 由于比较简单,基本知识主要是CURD。(创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete))。
    推荐参考
    link1,link2。
  2. 下面模仿C++ STL完成一段简单的单向链表的实现(对节点进行了简单封装)。
    (由于只是简单实现基本功能,很多部分并没有进行安全性和效率上考虑,测试不够完备。源码和当时实现部分注释附在下面。有兴趣的可以帮忙改进,感谢Thanks♪(・ω・)ノ)
#include 
using namespace std;

//定义节点
template <class T>
struct node
{
    T data;
    node* next;
    node() : data(T()), next(nullptr) {}; //T 类型必须含有完成的赋值函数,防止其他错误。
    node(T val) : data(val), next(nullptr) {};
    node(T val, node* ptr) : data(val), next(ptr) {};
    virtual ~node() {};
};

// 别名
using node_nums = unsigned long int;
//template 
//using list_node = node;
// 1.注释:
// 注意语法 typedef不能为模板类取别名
// 比如下语句无法通过编译:
/*
* template 
* typedef struct node list_node;
*/
// 2.如果强行使用,可以借助包裹一个类的方式实现

/*
* template 
* struct list_node {
*    typedef struct node node;
* };
*/
//使用时候必须如下使用:
/* list_node::node a;
* cout << a.data;
*/
// 3. 更方便的用法是借助using关键字,如上


// 为节点定义指针(迭代器)以及相关操作,方便使用
template<class T>
struct list_node_ptr {
    node<T>* iter;
    

    // 构造函数
    list_node_ptr() : iter(nullptr) {};

    list_node_ptr(T val) {
        iter = new node<T>(val);
    }

    list_node_ptr(T val, list_node_ptr ptr) {
        iter = new node<T>(val, ptr);
    }
    // 拷贝构造函数
    list_node_ptr(const list_node_ptr& x) : iter(x.iter) {}

    // 析构函数
    virtual ~list_node_ptr() {};

    // *iter 
    T& operator* () const {
        return (*iter).data;
    }
    // 注意 -> 为一元操作运算符,返回指向自身元素的指针
    node<T>* operator-> () const {
        return &(operator*());
    }

    // 为了简单方便,重载++表示类似ptr = ptr-> next含义。
    // 后置++ (ptr++)
    list_node_ptr<T> operator++ (int) {
        list_node_ptr<T> temp = *this; // 注意: 这里调用的是拷贝构造,不是*的重载。原因1.编译器首先遇到 “=”, 2. * 对本身元素,不是指向自己的指针
        ++*this;
        return temp;
    }

    // 前置++ (++ptr)
    list_node_ptr<T>& operator++() {
        iter = (*iter).next;
        return *this;
    }

};

//定义单向链表和基本操作
template <class T>
class forward_linklist
{
private:
    // 数据段
    list_node_ptr<T> head_node; // 头元素指针(迭代器)
    node_nums list_len; // 链表长度

    // 保证只有一个实例,以防使用时候出现异常错误
    forward_linklist() : head_node(), list_len(0) {};
    virtual ~forward_linklist() {
        forward_linklist_clear();
    }
    // 禁用拷贝
    forward_linklist(const forward_linklist& ptr) = delete;

public:

    // 两种初始化方式: 默认和列表初始化
    static forward_linklist* get_linklist() {
        return new forward_linklist();
    }

    static forward_linklist* get_linklist(const initializer_list<T>& lst) {
        auto ptr = new forward_linklist();
        for (const auto& val : lst) {
            ptr->add_node(val, ptr->get_len());
        }
        return ptr;
    }

    // 查找操作:
    list_node_ptr<T>& find_node(node_nums pos) {
        auto ptr = head_node;
        if (pos >= list_len) {
            return list_node_ptr<T>();// 暂时使用空表示查找失败
        }
        for (int i = 1; i < pos; ++i) {
            ++ptr;
        }
        return ptr;
    }

    // 删除操作:
    // 删除第i个元素(下标从0开始计数),返回删除的元素前一个元素指针(迭代器)
    // 如果是第一个元素,和失败一样返回nullptr
    list_node_ptr<T>& delete_node(node_nums pos) {
        if (pos >= list_len) {
            return list_node_ptr<T>();// 暂时使用空表示删除失败
        }
        else if (pos == 0) { // 表示删除第一个元素
            auto temp = head_node++;
            delete temp.iter;
            --list_len;
            return list_node_ptr<T>();
        }
        auto prior_ptr = find_node(pos - 1);
        auto del_ptr = prior_ptr;
        ++del_ptr;
        auto next_ptr = del_ptr;
        ++next_ptr;
        delete del_ptr.iter;
        prior_ptr.iter->next = next_ptr.iter;  
        --list_len;
        return prior_ptr;
    }

    // 修改操作:
    // 修改第pos个元素,值改为val,返回修改后的元素迭代器
    list_node_ptr<T>& update_node(T val, node_nums pos) {
        if (pos >= list_len) {
            return list_node_ptr<T>(); // 暂时使用空表示修改失败
        }
        auto ptr = find_node(pos);
        *ptr = val;
        return ptr;
    }

    // 插入操作:在第pos个元素后面进行插入元素操作(默认尾端插入)
    // 返回插入后的位置迭代器
    list_node_ptr<T>& add_node(T val, node_nums pos = list_len) {
        if (pos > list_len) {
            return list_node_ptr<T>();
        } else if (pos == list_len && pos == 0) { // 插入第一个元素
            head_node = list_node_ptr<T>(val);
            ++list_len;
            return head_node;
        }
        auto ptr = list_node_ptr<T>(val);
        auto insert_node = head_node;
        for (int i = 1; i < pos; ++i) {
            ++insert_node;
        }
        auto insert_next_node = insert_node;
        ++insert_next_node;
        insert_node.iter->next = ptr.iter;
        ptr.iter->next = insert_next_node.iter;
        ++list_len;
        return ptr;
    }
    // 清空所有元素
    void forward_linklist_clear() {
        while (list_len) {
            list_node_ptr<T> ptr = head_node++;
            delete ptr.iter;
            --this->list_len;
        }
    }
    // 返回长度
    node_nums get_len() const {
        return list_len;
    }

    // 修改链表长度 (!!!不安全,考虑简单实现没有继续改进)
    node_nums& change_len(const node_nums& len){
        list_len = len;
    }

    // 交换
    void swap(forward_linklist<T>& lst1) {
        auto lst = lst1;
        lst1.head_node = this->head_node;
        lst1.change_len(this->get_len());
        this->head_node = lst.head_node;
        this->list_len = lst.get_len();
    }
};

测试代码:

// 测试 int类型;
//本来想顺便测试char*(实际C++中建议不要使用裸指针)
// 没时间算了(晕)

// 遍历一次 
void print(forward_linklist<int>* head) {
    cout << endl;
    int len = head->get_len();
    cout << "The current length is " << len << endl;
    int pos = 0;
    auto ptr = head;
    while (pos < len) {
        cout << *(ptr->find_node(pos++)) << " ";
    }
    cout << endl;

}
int main()
{
    auto list_head1 = forward_linklist<int>::get_linklist();
    auto list_head2 = forward_linklist<int>::get_linklist({ 1,2,3,4,5 });

    print(list_head1);
    print(list_head2);

    cout << "find pos 1 elem : " << *list_head2->find_node(1) << endl << endl;
    cout << "______________" << endl;
    cout << "delete pos 1 elem : " << *list_head2->delete_node(1) << endl;
    cout << "______________" << endl;
    print(list_head2);
    cout << "insert pos 3 elem (100):" << *list_head2->add_node(100, 3) << endl << endl;
    print(list_head2);
    cout << "______________" << endl;
    cout << "update pos 4 elem" << *list_head2->update_node(1000,4)<< endl;
    cout << "______________" << endl;


    return 0;
}

双向链表

由于双向链表仅仅是单向链表添加了一个prior指针,许多帖子已经总结得很好了。这里不仔细总结了。可以推荐参考:link1, link2.

拓展与补充:

  1. 循环链表(将单链表头尾节点连接)
  2. 双向循环链表(基于双向链表+头尾节点连接)
  3. 异或链表(一种空间优化后,能够实现双向链表一样目的)
  4. 广义表(线性表的一种推广)

    其他许多结构,可以理解为基于以上基础的线性链表的优化。这种优化需要根据场景自行构造。

C++11常见API

这里假设容器存放元素类型为int(由于与vector类似很多,这里不仔细举例)

list

  1. 迭代器

    //注意和vector,array差别,他们都是容器迭代器双向随机迭代器,而这个并不是。
    //以下操作list不能使用 iter[i]; 
    //iter -=i ,iter += i , iter + i, iter - i; 
    //iter1 < iter2; iter1 <= iter2; iter1 > iter2; iter1 >= iter2;
    
    //可以使用的有
    # include 
    vector<list> ls{1,2,3,4,5};
    auto iter1 = ls.begin()
    auto iter2 =  ls.end();
    
    iter1++;
    ++iter1;
    --iter1;
    iter1--;
    *iter;
    iter1 == iter2;
    iter1 != iter2;
    
    //提供begin(),end();
    //rbegin(),rend();
    //cbegin(),cend();
    //crbrgin(),crend();这些迭代器具体含义和数组篇同理,省略
    
  2. 创建和初始化

    //参考vector
    list<int> ls1({1,2,3,4,5});
    list<int> ls2{4,10,17,30};
    list<int> ls3(-10);
    list<int> ls4(10 , 5);
    list<int> ls5(ls4);
    
  3. CURD操作
    假设2中容器都存在

    //容量大小(和vector一样)
    // empty, size, max_size,resize
    
    // 查找
    front(); back();//查找第一个或最后一个元素引用
    
    //修改
    assign();//基本同vector;
    assign(n,10);ls2.assign(ls1.begin(),ls1.end());
    ls1.assign({-1,-3,-5});
    
    // 插入元素
    emplace(),insert();
    push_front(),emplace_front(); //与push_back()等想反,在头部插入元素
    push_back();emplace_back();//以上函数基本和vector用法一致
    
    splice() //将一个链表该部分插入到另一个链表的指定位置
    
    //删除操作
    pop_front(),pop_back();
    erase();clear(); //略
    remove(val);//删除容器中所有等于 val 的元素
    remove_if();//删除容器中满足条件的元素。
    unique();//删除容器中相邻的重复元素,只保留一个。
    
    //其他
    merge();//合并两个有序链表,保证合并后链表依旧有序
    sort();//排序
    reverse();//反转容器元素顺序
    swap();//交换两个容器中的元素,要求容器以及存储元素类型一致
    

forward_list

基本和上面内容一样,具体参考reference部分forward_list。
主要注意:
forward_list迭代器基于list的相比是单向的。不支持‘–’(减减)运算符,同样也不是随机的。
还有部分API不同,如erase_after,insert_after,splice_after


典型试题

单一链表场景

链表翻转

  1. 反转链表
    本题建议需要背下所有方案。(摆手:没法,太经典了)
    思路
    a.就地翻转:就地把next指向修改,注意迭代修改内容。
    b.头插法:添加一个辅助的头结点,将元素一个一个插入到头结点部分。
    c.递归:需要假设后面已经逆转(递归函数压栈后,返回前处理步骤),需要将尾巴部分改变指针,返回头结点。
    当全部返回后,即可。
    核心代码
    a. 就地翻转
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    
    class Solution {
    public:
        ListNode* reverseList(ListNode* head) {
            ListNode* prior_node = nullptr, *cur_node = head; // 前一个节点,翻转的当前节点
            while (cur_node) {
                ListNode* next_node = cur_node->next; //翻转后的节点需要临时记录,以便迭代
                cur_node->next = prior_node; // 直接翻转
                // 移动到下一个
                prior_node = cur_node; 
                cur_node = next_node;
            }
            return prior_node; // 返回头结点(最后状态是 prior_node - nullptr(cur_node) - nullptr(next_node))
        }
    };
    
    b.头插法
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    class Solution {
    public:
        ListNode* reverseList(ListNode* head) {
            if (head == nullptr) {
                return head;
            }
            ListNode* dummy = new ListNode(0);// 使用一个辅助头结点进行头插法
            while (head) {
                ListNode* next_head = head->next;  //保存一下下一个待处理结点
                head->next = dummy->next; //将当前节点挂上去
                dummy->next = head;
                head = next_head;
            }
            head = dummy->next; //指向新的首个节点,释放辅助头结点内存
            delete dummy;
            return head;
        }
    };
    
    c.递归
    		/**
    		 * Definition for singly-linked list.
    		 * struct ListNode {
    		 *     int val;
    		 *     ListNode *next;
    		 *     ListNode(int x) : val(x), next(NULL) {}
    		 * };
    		 */
    		class Solution {
    		public:
    		    ListNode* reverseList(ListNode* head) {
    		        //出发点假设其他的部分已经反转,如何反转其他部分
    		        // 停止递归调用(链表长度为小于等于1时)
    		        if (head == nullptr || head->next == nullptr) {
    		            return head;
    		        }
    		        //递归,找到倒数第1个节点(或者是新的头结点) new_head,当前head节点为倒数第二个
    		        ListNode* new_head = reverseList(head->next);
    		        head->next->next = head; //改变指向 (由于new_head是新的头,这里写new_head->next = head不对, 注意递归函数返回时候处理部分中间情况应该为 .... ->head-> new_tail<-.....new_head)
    		        head->next = nullptr;// head成为新的部分的尾巴
    		        return new_head; //返回改变后的头结点
    		    }
    		};
    

    三个方法时间复杂度O(n)。
    空间复杂度除了最后是O(n),其他两个为O(1)。

  2. 回文链表
    思路
    a. 存在一个数组中,双指针两头一一匹配即可。(略)
    b.利用递归函数:
    递归函数一来一回能够使得经过链表每一个节点两次。只要记住对称位置,就能够进行比较
    c. 双指针:该方案需要中途修改链表(注意!)
    首先用快慢指针找到前半部分尾巴。
    翻转后半部分链表
    一个一个判断是否回文;
    还原链表即可
    核心代码
    b.递归
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    class Solution {
        ListNode* front_ptr;
    public:
        bool isPalindrome(ListNode* head) {
            front_ptr = head;
            return recursively_check(head);
        }
        bool recursively_check(ListNode* node) {
            if (node != nullptr) {
                if (!recursively_check(node->next)) { // 开始递归,找到最后一个元素比较后返回值
                    return false;
                }
                 // 执行过程是先到尾结点后一个,进行判断,后面接着返回
                if (node->val != front_ptr->val) {
                    return false;
                } else {
                    front_ptr = front_ptr->next; // 修改全局对称指针,继续判断
                }
    
            }
            return true; // 最后空节点是,先预设为true;递归返回时候继续处理
        }
    };
    
    c. 双指针(快慢指针)
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    class Solution {
    public:
        ListNode* find_middle_node(ListNode* head) {
            ListNode* slow = head, * fast = head;
            while (fast && fast->next) {
                slow = slow->next;
                fast = fast->next->next;
            }
            return slow;
        }
    
        ListNode* reverse_list(ListNode* head) {
            ListNode* prev = nullptr, * cur = head;
            while(cur) {
                ListNode* cur_next = cur->next;
                cur->next = prev;
                prev = cur;
                cur = cur_next;
            }
            return prev;
        }
    
        bool isPalindrome(ListNode* head) {
            if (head == nullptr) { //排除空链表
                return true;
            }
            // 找中点或其后面一位
            ListNode* middle_node = find_middle_node(head);
            // 翻转后面部分
            ListNode* new_head = reverse_list(middle_node);
            // 进行判断
            ListNode* cur1 = new_head;
            ListNode* cur2 = head;
            bool flag = true;
            ListNode* prev_tail = nullptr;
            while(cur1 && cur2) {
                if (prev_tail == nullptr && (cur1->next == nullptr || cur1->next->next== nullptr)) { // 小优化,后面无需找前半部分最后一个节点
                    prev_tail = cur1;
                }
                if (cur1->val != cur2->val) { // 进行比较
                    flag = false;
                } //由于之前优化部分,所以必须要找到中点前一个节点,这里不能比较错误直接跳出循环
                    cur1 = cur1->next;
                    cur2 = cur2->next;
            }
            //还原链表
            new_head = reverse_list(new_head);
            prev_tail->next = new_head;
            return flag;
    
        }
    };	
    

    三个方法时间复杂度O(n)。
    空间复杂度除了第二个是O(n),其他两个为O(1)

其他补充:
重排链表
k个一组翻转链表
从头到尾打印链表

环形链表

环路检查
思路
a. 哈希:直接记录节点,遇到同样节点直接返回(略)
b.快慢指针:可以看看题解(主要注意计算一下)
核心代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* slow = head, *fast = head;
        // 找到第一次相遇点
        while(fast && fast->next) {
            slow = slow ->next;
            fast = fast ->next->next;
            if (slow == fast) { // 找到后,慢指针路程为1份,快指针路程为1份+k个环的长度+一段
                fast = head;
                while (fast != slow) { // 将快指针重新置于起点,进行一步一步遍历,抵消1份中不是环的长度
                    fast = fast->next;
                    slow = slow->next;
                }
                return slow;
            }
        }
        return nullptr;
    }
};

时间复杂度:O(n)。
空间复杂度:O(1)。

查找和删除链表节点

  1. 返回倒数第 k 个节点
    思路
    快慢指针,比较容易。
    这里注意k有效。如果k可能无效。这里另外讨论函数如何定义。
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    class Solution {
    public:
        int kthToLast(ListNode* head, int k) {
            ListNode* pa = head, *pb = head;
            for (int i = 0; i < k && pa; ++i) { // 防止k无效,导致pa越界
                pa = pa -> next;
            }
            while (pa) {
                pa = pa->next;
                pb = pb->next;
            }
            return pb->val;
        }
    };
    

    时间复杂度O(n)。
    空间复杂度O(1)。

  2. 删除链表中的节点
    思路
    无法删除自己,只好变为自己下一个元素,删除下一个元素(前提自身不是尾结点)。
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode(int x) : val(x), next(NULL) {}
     * };
     */
    class Solution {
    public:
        void deleteNode(ListNode* node) {
            node -> val = node->next->val;
            node->next = node->next->next;
        }
    };
    

    时间复杂度O(1)。
    空间复杂度O(1)。

  3. 删除排序链表中的重复元素
    思路
    类似数组删除重复元素,只是这个可以直接指向下一个节点(LeetCode这里可以暂时考虑不用delete和free。这个是制作这个节点的对象考虑的问题,并且不知道指针这块内存如何获得。如果明确是在堆中获得,需要考虑释放。)
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        ListNode* deleteDuplicates(ListNode* head) {
            if (head == nullptr || head->next == nullptr) { //空节点或一个节点
                return head;
            }
    
            ListNode* slow = head, *fast = head->next; //初始条件
            while (fast) {
                if (slow->val == fast->val) {
                    slow->next = fast->next;
                } else {
                    slow = slow->next;
                }
                fast = fast->next; // 两句判断语句都要做fast更新,只是对slow的更新不一样
            }
            return head;
        }
    };
    

    时间复杂度O(n)。
    空间复杂度O(1)。

其他补充:
删除排序链表中的重复元素 II
删除链表的节点

链表排序

排序链表
思路
a. 插入排序:使用插入排序,注意代码细节
b. 归并排序:这里介绍自底向上排序方法,另一种自顶向下可以参考这里:
由于其他logn的排序不是变化相邻元素(往往需要随机范文),归并排序能够做到只访问相邻元素。
主要思想是模仿归并排序进行code。
难度在于:代码细节较多,控制要求较高。
c.堆排序:考虑堆结构,将所有节点放入堆中,不停重建链表。
核心代码
a.插入排序

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* insertionSortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        } 

        ListNode* dummy = new ListNode(0 , head); // 头插法进行插入排序
        ListNode *prev = nullptr, *cur = head->next; // 排序需要移动元素必须指针
        ListNode* last_sort_node = head; // 已经排好序元素分界线
        while (cur) {
            if (last_sort_node -> val <= cur -> val) {
                last_sort_node = last_sort_node -> next; // 扩展有序段,能够保证如果没有调整,last_sort_node->next == cur
            } else { //需要进行调整
                ListNode* prev = dummy; 
                while (prev -> next -> val <= cur -> val) { // 找到插入节点前一个位置,方便插入
                    prev = prev -> next;
                }
                // 进行调整
                last_sort_node->next = cur -> next; // 移除调整节点
                cur ->next = prev->next; //单链表插入首先将插入节点next修改(被插入位置修改后,找不到后继节点)
                prev->next = cur;
            }
            cur = last_sort_node -> next; //这里考虑修改后节点cur指向,所以变成了 last_sort_node -> next
        }
        cur = dummy -> next;
        delete dummy;
        return cur;
    }
};

b. 归并排序

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        int len = 0;
        ListNode* node = head;

        // 计算链表长度
        while (node) {
            ++len;
            node = node -> next;
        }

        // 使用一个辅助头节点
        ListNode* dummy = new ListNode(0, head);
        // 每次左移一位,表示该处链表归并排序完成
        for (int sublen = 1; sublen < len; sublen <<= 1) {
            ListNode* prev = dummy, * cur = dummy->next; //有可能第一个节点换掉了,所以这里不能写* cur = head
            while (cur) {
                ListNode* head1 = cur;  //第一段节点的第一个节点, 下面找第二段节点的第一个节点
                for (int i = 1; i < sublen && cur->next; ++i) { //防止有一部分节点长度不为sublen
                    cur = cur->next;
                }
                ListNode* head2 = cur->next; //当前 cur 为第二段节点的第一个节点前一个节点
                cur->next = nullptr; //第一段与第二段断开
                
                cur = head2;  //将第二段与后面部分断开,注意考虑第二段cur为空情况
                for (int i = 1; i < sublen && cur && cur->next; ++i) {
                    cur = cur->next;
                }
                ListNode* next_part = nullptr; //后面部分的头结点,为了防止cur为空,所以相比前一段分隔需要加上一个判断
                if (cur) {
                    next_part = cur->next;
                    cur->next = nullptr;
                }


                ListNode* merged = merge(head1, head2); // 合并两端,返回一个头结点
                prev->next = merged; //与上一个部分合并
                while (prev->next) {
                    prev = prev->next;
                } // prev指向合并后链表段最后一个元素
                cur = next_part; //更新最后一个节点
            }
        }
        node = dummy->next;
        delete dummy; 
        return node;
    }

    ListNode* merge(ListNode* head1, ListNode* head2) { //参考题目:合并两个有序列表题
        ListNode* dummy = new ListNode(0);
        ListNode* l1 = head1, *l2 = head2, *cur = dummy;
        while (l1 && l2) {
            if (l1->val < l2->val) {
                cur->next = l1;
                l1 = l1->next;
            }else {
                cur->next = l2;
                l2 = l2->next;
            }
            cur = cur->next;
        }

        while (l1) {
            cur->next = l1;
            l1 = l1->next;
            cur = cur->next;
        }

        while (l2) {
            cur->next = l2;
            l2 = l2->next;
            cur = cur->next;
        }
        cur = dummy->next;
        delete dummy;
        return cur;
    }
};

c.堆排序

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
    struct node {
        int val;
        ListNode* ptr;
        bool operator> (const node& that) const { //构建小根堆,greater,重载大于号
            return val > that.val;
        }
    };
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) {
            return head;
        }
        priority_queue <node,vector<node>,greater<node>> pq; //注意修改排序方式,默认为大根堆;
        while (head) {
            pq.push({head->val, head}); 
            head = head->next;
        }
        ListNode* dummy = new ListNode(0);
        ListNode* cur = dummy;
        while (!pq.empty()) {
            auto p = pq.top();
            pq.pop();
            p.ptr->next = nullptr;
            cur->next = p.ptr;
            cur = cur->next;
        }
        cur = dummy->next;
        delete dummy;
        return cur;
    }
};

方法a:时间复杂度:O( n 2 n^2 n2)。
空间复杂度:O(n)。
方法b:时间复杂度:O(nlogn)。
空间复杂度O(1)。
方法c:时间复杂度:O(nlogn)。
空间复杂度O(n)。

其他补充:
对链表进行插入排序

多个链表场景

链表相交

两个链表的第一个公共节点
思路
a.直接利用哈希表,如果命中一个。我们需要返回命中的那一个。(略)
b.双指针,同时从headA,headB出发,如果对于其中一个指针第一次为空指向另个一节点头继续遍历;当且仅当出现相同节点时候,如果非空:有相交节点(当前指向);否则没有。
核心代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        if (headA == nullptr || headB == nullptr) { // 只要一个为空节点,保证不相交
            return nullptr;
        }
        ListNode* pA = headA, *pB = headB;
        while (pA  != pB) { // 注意,最差情况:pA,pB以不同顺序一定会把两个链表访问一次
        //不相交一定会有pa == pb ==nullptr
            pA  = pA  == nullptr? headB : pA ->next;
            pB = pB == nullptr? headA : pB->next;
        }
        return pA;
    }
};

时间复杂度O(n+m)。
空间复杂度O(1)。

合并和分隔链表

  1. 合并两个有序链表
    思路
    a. 迭代(两个指针同时移动操作即可):注意原节点可能长度不一,导致后面需要添加尾巴。
    b. 递归:
    核心代码
    a. 迭代
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
            ListNode *p1 = list1, *p2 = list2;
            ListNode *dummy = new ListNode(0); // 添加一个辅助的头结点,比较好操作
            ListNode *p = dummy; 
            while (p1 && p2) { // 两个指针指向有效元素
                if (p1 -> val < p2 -> val) {
                    p -> next = p1;
                    p1 = p1->next;
                } else {
                    p -> next = p2;
                    p2  = p2 -> next;
                }
                p = p->next; //当前合并后链表节点指针后移
            }
            // 将剩下节点全部合并到新节点中
            while (p1) {
                p -> next = p1;
                p1 = p1 -> next;
                p = p -> next;
            }
            while (p2) {
                p -> next = p2;
                p2 = p2 -> next;
                p = p -> next;
            }
            // 注意释放辅助节点
            p = dummy->next;
            delete dummy;
            return p;
        }
    };
    
    b. 递归
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
            if(list1 == nullptr) {
                return list2;  //返回非空的头结点
            } else if (list2 == nullptr) {
                return list1;
            } else if (list1 -> val < list2 -> val) { //将问题变为list1的后半部分和list2合并,返回头结点(这里将函数压入系统栈过程,实际在将问题规模简化)
                list1->next = mergeTwoLists(list1->next, list2); //返回的头结点就是list1的下一个节点,做一定处理(注意每次处理是递归函数返回阶段)
                return list1;
            } else {
                list2 -> next = mergeTwoLists(list2->next,list1); 
                return list2;
            }
            return nullptr;
        }
    };
    

    a.迭代:时间复杂度O(n + m)。
    空间复杂度O(1)。
    b.递归:时间复杂度O(n + m)。
    空间复杂度O(n + m)。

  2. 合并K个升序链表
    思路
    最直接的思路是两两合并,但是时间复杂度为O( k 2 n k^2n k2n),这里我们给出一些优化方法。
    a.分治法,本题可以考虑排序链表中的归并排序方法。
    b.使用优先队列,本题考虑排序链表题目中堆排序方法。(基本同排序链表中代码一致,这里省略)
    核心代码
    a. 分治法(递归版本)
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        ListNode* mergeKLists(vector<ListNode*>& lists) {
            return merge(lists, 0, lists.size() - 1);
        }
    
        ListNode* merge_twolist(ListNode* headA, ListNode* headB) { //旧题目:合并两个有序链表,另一种简单写法
            if ((!headA) || (!headB)) {
                return headA ? headA : headB; //返回非空链表头结点
            }
            ListNode head, *cur = &head, *ptra = headA, *ptrb = headB;
            while (ptra && ptrb) {
                if (ptra -> val < ptrb ->val) {
                    cur->next = ptra;
                    ptra = ptra->next;
                } else {
                    cur->next = ptrb;
                    ptrb = ptrb->next;                
                }
                cur = cur->next;
            }
    
            cur->next = (ptra? ptra : ptrb);
            return head.next;
        }
    
        ListNode* merge(vector<ListNode*>& lists, int l, int r) {
            if (l == r) {
                return lists[l]; //分开
            } else if (l > r) {
                return nullptr; // 没有排序对象
            }
            int mid = l + ((r - l) >> 1);
            return merge_twolist(merge(lists, l, mid), merge(lists, mid + 1, r));
        }
    };	
    

    a.分治法:时间复杂度O(knlogk)。
    空间复杂度O(logk)
    b.堆排序:时间复杂度O(knlogk)。
    空间复杂度O(k)

  3. 分隔链表
    思路
    计算长度后,注意分开时候部分长度计算,还有注意断开为空时候条件判断。
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        vector<ListNode*> splitListToParts(ListNode* head, int k) {
            // 首先统计链表长度,然后根据长度进行分隔即可
            int len = 0;
            ListNode* cur = head;
            while (cur) {
                cur = cur->next;
                ++len;
            }
            vector<ListNode*> ans(k, nullptr);
            //每段至少分为part_len个,余下的部分前面部分一个part一份
            int part_len = len / k, reminder = len % k;
            cur = head;
            for (int i = 0; i < k && cur; ++i) { //注意 cur为空时,计算完长度后,与下一部分断开code会报错 
                ans[i] = cur;
                int part_size = part_len + (i < reminder? 1 : 0); //具体每一个部分的长度
                for (int j = 1; j < part_size; ++j) {
                    cur = cur->next;
                }
                ListNode* cur_next = cur->next;
                cur->next = nullptr; //断开
                cur = cur_next;
            }
            return ans;
        }
    };
    

    时间复杂度O(n)。
    空间复杂度O(1)

其他补充:
1.合并两个链表
2.分割链表

链表的运算

两数相加
思路
本题代码基本和合并两个链表类似。
注意进位和两个数字位数不等的时候。
核心代码

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { 
        // 注意这里数据存储按照类似小端方式
        ListNode dummy, *tail = &dummy;

        bool carry = false; //表示是否进位

        while (l1 || l2) { // l1与l2全部为空,结束循环
            int n1 = l1 ? l1 -> val : 0;
            int n2 = l2 ? l2 -> val : 0;
            int sum = n1 + n2 + carry;
            
            //更新节点数据和进位数据
            ListNode* next_cur = new ListNode(sum % 10);
            tail->next = next_cur;
            tail = tail->next;
            carry = sum / 10 == 1 ? true : false;

            // 更新l1,l2
            if (l1) {
                l1 = l1 -> next;
            }

            if (l2) {
                l2 = l2 -> next;
            }
        }

        // 注意可能进位操作,多一位
        if(carry) {
            tail->next = new ListNode(1);
        }
        return dummy.next;
    }
};

时间复杂度O(n)。
空间复杂度O(1)。

其他补充:
链表求和
两数相加 II

其他题型

  1. 旋转链表
    思路
    a 查找倒数第k个节点前一个节点,然后断开。把原始前面部分接在断开的后面部分(注意k>len。所以要先求链表长度)(略)
    b 将链表做成环状,然后根据上面断开。(两个都需要注意代码细节)
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
    public:
        ListNode* rotateRight(ListNode* head, int k) {
            if (k == 0 || head == nullptr || head->next == nullptr) { //排除不需要继续算的数据
                return head;
            }
    
            // 由于要连接成环,我们需要定位在最后一个节点上。
            int len = 1;
            ListNode* cur = head;
            while (cur -> next) {
                ++len;
                cur = cur->next;
            }
    
            // 如果旋转长度为len的倍数,直接返回
            k = len - k % len;
            if (k == len) {
                return head;
            }
    
            //封闭成环
            cur -> next = head;
            while (k--) {
                cur = cur->next;
            }
            // 断开并且记录新的头结点
            head = cur->next;
            cur->next = nullptr;
            return head;
        }
    };```
    >时间复杂度O(n)。
    空间复杂度O(1)
  2. 链表随机节点
    思路
    a. 最简单思路使用数组记录链表每一个节点,然后随机(略)
    b. 这里对空间优化(假设数据足够大),主要使用蓄水池算法:
    对第i个节点,每次投掷一个随机数[0,i);等于0返回该值,否则保留原始值。
    如果要随机k个数,改进为:
    1.创建一个长度为 k 的数组(蓄水池)用来存放结果;
    2.初始化为 arr 的前 k 个元素。
    3.从 k+1 个元素开始遍历直到数组结束,算法对每一个下标生成一个随机数 j∈[0,i), 如果 j < k, 那么蓄水池的第 j 个元素被替换为 arr 的第 i 个元素。
    核心代码
    /**
     * Definition for singly-linked list.
     * struct ListNode {
     *     int val;
     *     ListNode *next;
     *     ListNode() : val(0), next(nullptr) {}
     *     ListNode(int x) : val(x), next(nullptr) {}
     *     ListNode(int x, ListNode *next) : val(x), next(next) {}
     * };
     */
    class Solution {
        ListNode* head;
    public:
        Solution(ListNode* head) {
            this->head = head;
        }
        
        int getRandom() {
            int ans = 0, pos = 1;
            for (auto node = head; node; pos++, node = node -> next) {
                if (rand() % pos == 0) {
                    ans = node -> val; //改变数据
                }
            }
            return ans;
        }
    };
    
    /**
     * Your Solution object will be instantiated and called as such:
     * Solution* obj = new Solution(head);
     * int param_1 = obj->getRandom();
     */
    

    蓄水池抽样方法:时间复杂度初始化O(1),随机采样O(n)。
    空间复杂度均为O(1)。

  3. 复制带随机指针的链表
    思路
    本题可以有比较有技巧性的解法。
    a. 最简单的解法是使用哈希表,首先将源节点和新节点一一对应记录(同时生成);
    第二次遍历时候,一个一个连接好即可。
    b. 上一题使用空间复杂度为O(n),如果要求空间复杂度为O(1),且允许修改元链表,如何优化?
    优化方法:
    1.原地拷贝节点:如1->3->4变为1->1->3->3->4->4。
    2.将新建的拷贝节点的随机指针设置好。
    3.分开拷贝节点部分(随机节点可以不动,只需要改变next)
    c. 迭代回溯做法参考题解。
    核心代码
    a.哈希表直接求解
    /*
    // Definition for a Node.
    class Node {
    public:
        int val;
        Node* next;
        Node* random;
        
        Node(int _val) {
            val = _val;
            next = NULL;
            random = NULL;
        }
    };
    */
    
    class Solution {
    public:
        Node* copyRandomList(Node* head) {
            if (head == nullptr) {
                return head;
            }
    
            unordered_map<Node*, Node*> mp;
            
            // 创建映射关系[旧节点,新节点]
            for(auto node = head; node; node = node -> next) {
                Node* new_ptr = new Node(node->val);
                mp[node] = new_ptr;
            }
    
            //依次串好
            for (auto[ori_node , new_node] : mp) {
                new_node->next = mp.count(ori_node->next) ? mp[ori_node->next] : nullptr; //注意指向空节点,mp内没有
                new_node->random = mp.count(ori_node->random) ? mp[ori_node->random] : nullptr; 
            }
    
            return mp[head];
        }
    };
    
    b 直接迭代:
    /*
    // Definition for a Node.
    class Node {
    public:
        int val;
        Node* next;
        Node* random;
        
        Node(int _val) {
            val = _val;
            next = NULL;
            random = NULL;
        }
    };
    */
    
    class Solution {
    public:
        Node* copyRandomList(Node* head) {
            if (head == nullptr) {
                return nullptr;
            }
            //首先原地拷贝
            for (auto node = head; node; node = node->next->next) {
                Node* new_node = new Node(node->val);
                new_node -> next = node -> next;
                node->next = new_node;
            }
    
            //然后将new_node的随机指针设置好
            for (auto node = head; node; node = node->next->next) {
                Node* new_node = node->next;
                new_node->random = node->random ?  node->random->next : nullptr;
            }        
    
            Node* new_head = head->next; // 记录新的头结点
            // 最后将两个部分分离
            for (auto node = head; node; node = node->next) { //分离时候由于指针改变,只要移动一次
                Node* new_node = node->next;
                node->next = new_node->next;
                new_node->next = new_node->next ? new_node->next->next : nullptr;
            }
            return new_head;
        }
    };
    

    a,b方法时间复杂度O(n)。
    a方法空间复杂度O(n),b方法空间复杂度O(1)。


References:

  1. 基础知识部分参考:https://www.bilibili.com/video/BV13f4y1k7kw
  2. list部分: https://en.cppreference.com/w/cpp/container/list
  3. forward_list部分:https://en.cppreference.com/w/cpp/container/forward_list
  4. 题目来源:https://leetcode-cn.com/
  5. 其他推荐阅读:https://zhuanlan.zhihu.com/p/150871816
欢迎评论指正和补充

你可能感兴趣的:(数据结构和算法,基础数据结构,链表,数据结构,算法)