数据结构刷题:第七天

简介: 具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

一,环形链表


141. 环形链表 - 力扣(LeetCode)

https://leetcode.cn/problems/linked-list-cycle/?plan=data-structures&plan_progress=ggfacv7


642d1896a2d4491497517d50c14579de.png


1,哈希表


思路及算法


最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。


具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。


class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};


复杂度分析


时间复杂度:O(N),其中 N 是链表中的节点数。最坏情况下我们需要遍历每个节点一次。


空间复杂度:O(N),其中 N 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。


2,快慢指针


思路及算法


本方法需要读者对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。


假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。


我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。


细节


为什么我们要规定初始时慢指针在位置 head,快指针在位置 head.next,而不是两个指针都在位置 head(即与「乌龟」和「兔子」中的叙述相同)?


观察下面的代码,我们使用的是 while 循环,循环条件先于循环体。由于循环条件一定是判断快慢指针是否重合,如果我们将两个指针初始都置于 head,那么 while 循环就不会执行。因此,我们可以假想一个在 head 之前的虚拟节点,慢指针从虚拟节点移动一步到达 head,快指针从虚拟节点移动两步到达 head.next,这样我们就可以使用 while 循环了。


当然,我们也可以使用 do-while 循环。此时,我们就可以把快慢指针的初始值都置为 head。


class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return false;
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (slow != fast) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};


复杂度分析


时间复杂度:O(N),其中 N 是链表中的节点数。


当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。


当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮。


空间复杂度:O(1)。我们只使用了两个指针的额外空间。


二,合并两个有序链表


21. 合并两个有序链表 - 力扣(LeetCode)

https://leetcode.cn/problems/merge-two-sorted-lists/?plan=data-structures&plan_progress=ggfacv7

80293cfc2cbb4933968dc990bf769a5b.png

1,递归


我们可以如下递归地定义两个链表里的merge 操作

(忽略边界情况,比如空链表等) :



list1[0] + merge(list1[1 :],list2)     list1[0] < list2[0]

list2[0] + merge(list1, list2[1 :)    otherwise

也就是说,两个链表头部值较小的一个节点与剩下元素的merge 操作结果合并。


算法


我们直接将以上递归过程建模,同时需要考虑边界情况。


如果11或者12 一开始就是空链表,那么没有任何操作需要合并,所以我们只需要返回非空链表。否则,我们要判断11和12哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。如果两个链表有一个为空,递归结束。


class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == nullptr) {
            return l2;
        } else if (l2 == nullptr) {
            return l1;
        } else if (l1->val < l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};


复杂度分析


●时间复杂度:O(n+m),中n和m分别为两个链表的长度。因为每次调用递归都会去掉11或者12 的头节点(直到至少有一个链表为空),函数mergeTwoList多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即O(n + m)。

●空间复杂度: O(n+n),中n和1分别为两个链表的长度。递归调用mergeTwoLists函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时mergeTwoLists 函数最多调用n+m次,因此空间复杂度为O(n + m)。


2,迭代


思路


我们可以用迭代的方法来实现上述算法。当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。


算法


首先,我们设定一个哨兵节点 prehead ,这可以在最后让我们比较容易地返回合并后的链表。我们维护一个 prev 指针,我们需要做的是调整它的 next 指针。然后,我们重复以下过程,直到 l1 或者 l2 指向了 null :如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。否则,我们对 l2 做同样的操作。不管我们将哪一个元素接在了后面,我们都需要把 prev 向后移一位。


在循环终止的时候, l1 和 l2 至多有一个是非空的。由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。这意味着我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。


下图展示了 1->4->5 和 1->2->3->6 两个链表迭代合并的过程:


class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* preHead = new ListNode(-1);
        ListNode* prev = preHead;
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val < l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }
            prev = prev->next;
        }
        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev->next = l1 == nullptr ? l2 : l1;
        return preHead->next;
    }
};


复杂度分析


时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。


空间复杂度:O(1)。我们只需要常数的空间存放若干变量。


三,移除链表元素


203. 移除链表元素 - 力扣(LeetCode)

https://leetcode.cn/problems/remove-linked-list-elements/?plan=data-structures&plan_progress=ggfacv7

547aa5772f444923b5bab521b764b9e9.png

1,递归


链表的定义具有递归的性质,因此链表题目常可以用递归的方法求解。这道题要求删除链表中所有节点值等于特定值的节点,可以用递归实现。


对于给定的链表,首先对除了头节点 head 以外的节点进行删除操作,然后判断 head 的节点值是否等于给定的 val。如果 head 的节点值等于val,则 head 需要被删除,因此删除操作后的头节点为 head.next;如果 head 的节点值不等于 val,则head 保留,因此删除操作后的头节点还是 head。上述过程是一个递归的过程。


递归的终止条件是 head 为空,此时直接返回 head。当head 不为空时,递归地进行删除操作,然后判断head 的节点值是否等于 val 并决定是否要删除head。


class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) {
            return head;
        }
        head->next = removeElements(head->next, val);
        return head->val == val ? head->next : head;
    }
};


复杂度分析


时间复杂度:O(n),其中 n 是链表的长度。递归过程中需要遍历链表一次。


空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用栈,最多不会超过 n 层。


2,迭代


也可以用迭代的方法删除链表中所有节点值等于特定值的节点。


用temp表示当前节点。如果temp的下一个节点不为空且下一个节点的节点值等于给定的val,则需要删除下一个节点。删除下一个节点可以通过而


以下做法实现:

temp.next = temp.next. next


如果temp 的下一个节点的节点值不等于给定的val,则保留下一个节点,将temp移动到下一个节点即可。当temp的下一个节点为空时,链表遍历结束,此时所有节点值等于val的节点都被删除。具体实现方面,于链表的头节点head有可能需要被删除,因此创建哑节点dummyHead,令dummyHead.next= head,初 始化temp =dummyHead,然后遍历链表进行删除操作。最终返回dummyHead.next 即为删除操作后的头节点。


class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        struct ListNode* dummyHead = new ListNode(0, head);
        struct ListNode* temp = dummyHead;
        while (temp->next != NULL) {
            if (temp->next->val == val) {
                temp->next = temp->next->next;
            } else {
                temp = temp->next;
            }
        }
        return dummyHead->next;
    }
};


复杂度分析


  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次。


  • 空间复杂度:O(1)。
目录
相关文章
|
6天前
|
存储 算法 调度
数据结构期末复习(3)栈和队列
数据结构期末复习(3)栈和队列
21 0
|
6天前
|
存储 算法 Serverless
数据结构期末复习(六)查找算法
数据结构期末复习(六)查找算法
12 0
|
6天前
|
存储 C语言
数据结构期末复习(2)链表
数据结构期末复习(2)链表
12 0
|
C语言
【C与数据结构】——寒假提高每日练习Day1
【C与数据结构】——寒假提高每日练习Day1
|
存储 算法
【C与数据结构】——寒假提高每日练习Day2
【C与数据结构】——寒假提高每日练习Day2
|
存储 算法 JavaScript
数据结构刷题:第六天
在第一次遍历时,我们使用哈希映射统计出字符串中每个字符出现的次数。在第二次遍历时,我们只要遍历到了一个只出现一次的字符,那么就返回它的索引,否则在遍历结束后返回 −1。
86 0
数据结构刷题:第六天
|
机器学习/深度学习 算法 C++
数据结构刷题:第四天
数据结构刷题:第四天
68 0
数据结构刷题:第四天
|
算法
数据结构刷题:第五天
具体地,我们首先遍历该数组一次,如果某个元素为 0,那么就将该元素所在的行和列所对应标记数组的位置置为 true。最后我们再次遍历该数组,用标记数组更新原数组即可。
58 0
数据结构刷题:第五天
|
存储
数据结构刷题:第九天
当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
61 0
数据结构刷题:第九天
|
存储 算法
数据结构刷题:第十二天
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点 root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root 为根节点的整棵子树的翻转。
53 0
数据结构刷题:第十二天