Leetcode题解——双指针系列

115 阅读7分钟

合并有序数组

第一遍尝试

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

(第一版答案)
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        auto i = nums1.begin(), j = nums2.begin();
         while (n) {
            if (*i >= *j) {
                nums1.insert(i, *j);
                j++;
                n--;
            }
            else
            {
                i++;
            }
        }

    }

太久没写code的结果就是思维如此的不严谨,代码运行后不断出现内存溢出的错误。那么问题出在哪里呢?

上面的代码思路很简单,双指针,如果指针2指向的元素比指针1指向的元素小,则在指针1前面插入指针2指向的元素,且需遍历的元素减1(n--),否则指针1向前移动一格。那如果指针2的元素一直比指针1的元素大呢? 该循环不会停止直到造成内存溢出,即指针1超出数组1的范围。所以该版本只需实现当指针1走到原数组1第m个元素的时候停止i++且将剩余的数组2内容复制到指针1后面的空间即可。

官方答案

解法1

直接合并数组然后排序。

缺点:时间复杂度太高,为O((n+m)log(n+m))

观后感:这个解法真的可以说是最简单最直观的,但是我完全没有考虑过的!原因在于我的知识储备中不存在合并两个数组的函数,对于我来说合并数组需要手动实现,太过麻烦,所以直接排除。

解法2

既然解法1的缺点是时间复杂度,那么优化的方向就是减少时间复杂度。(这真的不是废话,缺点优化方向很重要的)

如何减少遍历数组的次数呢?因为数组本身是有序的,所以只要从前往后遍历两数组,每次比较大小,然后将较小值存入返回数组中即可,由于这边数组1是返回数组,所以如果直接在数组1上面读写会可能覆盖掉某些还未比较的值,所以需要将数组1原本的元素进行复制再比较。

缺点:空间复杂度略高,多占用了一个数组1的空间

解法3

所以现在的优化方向就变成了减少空间复杂度:有没有什么办法可以不需要复制数组1就进行比较读写且不会覆盖到还没有读写到的值呢?答案就是从后往前读写。由于数组1的空间等于数组1实际长度加数组2实际长度,考虑两种极端情况:

1.数组2的第一个元素比数组1的最后一个元素大(即数组2里的数都比数组1的数大),则代码会讲数组2copy到数组1后n格中,然后遍历数组1,结束代码。

  1. 数组1的第一个元素比数组2的最后一个元素大(即数组1里的数都比数组2的数大),则代码会将数组1往后移n格,然后将数组2的元素copy到数组1前n格中,结束代码。

综上所述,该代码不会发生元素被覆盖未被读写的情况。

(最终答案)
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        int index1 = m-1, index2 = n-1;
        int MergeIndex = m + n - 1;
        while(index1 >= 0 || index2 >= 0)
        {
            if(index1 < 0)
                nums1[MergeIndex--] = nums2[index2--];
            else if(index2 < 0)
                nums1[MergeIndex--] = nums1[index1--];
            else if(nums1[index1] < nums2[index2])
                nums1[MergeIndex--] = nums2[index2--];
            else
                nums1[MergeIndex--] = nums1[index1--];
        }

    }

环形链表

给定一个链表,判断链表中是否有环。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。如果链表中存在环,则返回 true 。 否则,返回 false 。

解法1

简单的一批,遍历所有链表,把遍历过的元素都存进哈希表中,遍历过程中检测有无重复即可

缺点:空间复杂度大

收获:C++的unordered_set,是无序的哈希表,表中存储的值就是对应的key,表中的值无法修改,只能增删。

解法2

快慢指针,你走1步我走2步,只要存在环,慢指针就必然会被快指针追上。

    bool hasCycle(ListNode *head) {
        if (head == NULL) {
        return false;
        }
        auto slow = head, fast = head;
        while(slow && fast && fast->next)
        {
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
            {
                return true;
            }
        }
        return false;
        
    }

收获:

  • 最开始的(head == nullptr)条件一定要记得加!很重要,不仅仅是影响性能,还涉及安全问题。
  • 由于之前听说过快慢指针,所以想到这个办法并不困难,一开始疑惑的只是while的停止条件,最初是利用了链表节点数目的上限值作为迭代条件,后来才发现,原来只需要以是否迭代到空指针为条件即可,因为列表的单向性,只要迭代到空指针,就意味着链表结束,不存在循环,就可以return了。否则的话就会一直循环直到快指针追上慢指针。

思考:上面的代码leetcode还是不能beat 100%, 是否意味着还能加速?如果快指针每次不是走2步而是走3步呢?会更快吗?因为相当于每次追赶2个节点,如果环的节点数是2的倍数,理论上来说追赶上的时间是会缩短一半,如果环的节点数不是2的倍数,那时间也只是与走2步的情况相当罢了。照这样来说岂不是快指针每次走的越多越好?但实际情况是运行速度并没有得到提升,原因我也不清楚,欢迎小伙伴们和我探讨。

通过删除字母匹配到字典里最长单词

给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到。如果答案不止一个,返回长度最长且字典顺序最小的字符串。如果答案不存在,则返回空字符串。

这题思路也很清晰了,首先要筛选出字典中符合要求的字符串,其次是对筛选出来的字符串用长度及字典顺序再进一步筛选得到最长字典顺序最小的那一个。

  • 第一步本质就是在给定字符串中找相应的子序列,利用双指针可完成。

  • 第二步就是简单的比较长度,长度相同的情况下在利用string::compare函数对比字典顺序即可

当然了两个步骤其实可以一起进行,每找到一个符合的字典选项就与当前找到的最长的字符串进行比较。

    bool match(string a, string b)
    {
        int i = 0, j = 0;
        while(i < a.length() && j < b.length())
        {
            if(a[i] == b[j])
            {
                i++;
                j++;
            }
            else
                i++;
        }
        if(j == b.length())
            return true;
        
        return false;
    }

    string findLongestWord(string s, vector<string>& dictionary) {

        string longest = "";
        for(auto i : dictionary)
        {
            if(match(s,i))
            {
                if(i.length() > longest.length())
                    longest = i;
                else if(i.length() == longest.length())
                {
                    if(i.compare(longest) < 0)
                    {
                        longest = i;
                    }
                }
            }
        }
        return longest;

    }

收获:学会了compare函数算不算收获?因为这题思路真的不难。。关键是这个我第一时间想到的解法竟然是官网最优解法,可是还是没有beat 100%,想知道哪个步骤可以优化。。u1s1,我对优化代码真的很感兴趣,但入门太难。。。