【LeetCode 刷题笔记】链表 哈希表(15-29)

链表

1.移除链表元素(203)

题目

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104]
  • 1 <= Node.val <= 50
  • 0 <= val <= 50

思路及题解

解法一 原链表删除元素
  • 删除某个节点,只需要上一个节点的 next 指针指向要删除的节点的下一个节点即可
  • 当下一个节点为空时,遍历结束,停止删除
  • 当头节点需要删除时,直接把 head 向前移动即可
  • 头节点为空时,直接返回
  • 可以优化的地方
    • pre指针可以不用,用 cur 、 cur -> next 、cur -> next -> next 表示即可
    • 头节点为空和不为空时操作方法不统一,需要用虚拟头节点
/**
 * 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* removeElements(ListNode* head, int val) {
        //删除头节点,注意头节点不为空
        while (head != NULL && head->val == val) {
            ListNode* p =head;
            head = head->next;
            // free(p);   注意c++用 delete 释放内存空间,c 才用 free
            delete p;
        }
        //头节点为空时
        if (head == NULL)
            return head;
        ListNode* pre = head;
        ListNode* cur = head->next;
        while (cur != NULL) {
            if (cur->val == val) {
                pre->next = cur->next;
                ListNode* p = cur;
                cur = pre->next;
                // free(p);
                delete p;
            }
            else{
                pre = pre->next;
                cur = cur->next;
            }
        }
        return head;
    }
};
解法二 虚拟头节点
  • 使用虚拟头节点可以使代码逻辑统一,无论是添加节点还是删除节点都建议使用虚拟头节点
/**
 * 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* removeElements(ListNode* head, int val) {
        ListNode* dummyHead = new ListNode; // 记得开辟内存空间
        dummyHead->next = head;
        ListNode* cur = dummyHead;

        while (cur->next != NULL) {
            if (cur->next->val == val) {
                ListNode* p = cur->next;
                cur->next = cur->next->next;
                delete p;
            } else { // 如果下一个节点还是 val 就一直删除,否则才向前
                cur = cur->next;
            }
        }

        // 恢复头节点
        head = dummyHead->next;
        delete dummyHead;
        return head;
    }
};

2.设计链表(707)

题目

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。
  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1
  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。
  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。
  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。
  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

示例:

输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]

解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2);    // 链表变为 1->2->3
myLinkedList.get(1);              // 返回 2
myLinkedList.deleteAtIndex(1);    // 现在,链表变为 1->3
myLinkedList.get(1);              // 返回 3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • 调用 getaddAtHeadaddAtTailaddAtIndexdeleteAtIndex 的次数不超过 2000

思路及题解

解法一 单项链表
  • 使用虚拟头节点,增加和删除元素变得更容易
  • 下面的代码中,for循环可以用while循环优化,代码更简洁
  • 注意C++内定义结构体的方式、在类内要创建成员变量、MyLinkedList实际上是构造器
class MyLinkedList {
public:
    // 定义链表节点结构体
    struct LinkedNode {
        int val;
        LinkedNode* next;
   		//可以写LinkedNode(int val),代码更简洁
        //下面的代码是在写这个结构体之前写的,所以新节点还要指向NULL是多余写法
        LinkedNode() : val(0), next(nullptr) {}
    };

    int size;
    LinkedNode* head;

    MyLinkedList() {
        head = new LinkedNode();
        size = 0;
    }

    int get(int index) {
        if (index >= size || index < 0)
            return -1;
        LinkedNode* cur = head->next;
        for (int i = 0; i < index; i++) {
            cur = cur->next;
        }
        // while 的写法:
        /*  while(index--){ // 如果--index 就会陷入死循环
            cur = cur->next;}
        */
        return cur->val;
    }

    void addAtHead(int val) {
        LinkedNode* Node = new LinkedNode();
        Node->val = val;
        Node->next = head->next;
        head->next = Node;
        size++;
    }

    void addAtTail(int val) {
        LinkedNode* Node = new LinkedNode();
        Node->val = val;
        Node->next = NULL;
        LinkedNode* cur = head;
        for (int i = 0; i < size; i++)
            cur = cur->next;
        // while 的写法
        /*        while(cur->next != nullptr){
            cur = cur->next;
        }
        */
        cur->next = Node;
        size++;
    }

    void addAtIndex(int index, int val) {
        if (index > size)
            return;
        LinkedNode* cur = head;
        LinkedNode* Node = new LinkedNode();
        Node->val = val;
        for (int i = 0; i < index && i < size; i++)
            cur = cur->next;
        /*        while(index--) {
            cur = cur->next;
        }
        */
        Node->next = cur->next;
        cur->next = Node;
        size++;
    }

    void deleteAtIndex(int index) {
        if (index >= size || index < 0)	//注意这里等于 size 也不行
            return;
        LinkedNode* cur = head;
        /*while (index--) {
            cur = cur->next;
        }*/
        for(int i=0;i<index;i++)
        cur = cur->next;
        LinkedNode* p = cur->next;
        cur->next = p->next;
        delete p;
        //注意释放内存后还要将其变为空指针,否则p为野指针,若一不小心使用后果不堪设想
        p = NULL;
        size--;
    }
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList* obj = new MyLinkedList();
 * int param_1 = obj->get(index);
 * obj->addAtHead(val);
 * obj->addAtTail(val);
 * obj->addAtIndex(index,val);
 * obj->deleteAtIndex(index);
 */

3.反转链表(207)

题目

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000

**进阶:**链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

思路及题解

解法一 迭代法(双指针)
  • 原地操作,改变next的指向,不浪费空间
  • 需要创建临时指针 p 指向 cur 的下一个节点,否则 cur 无法向前转移
/**
 * 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* reverseList(ListNode* head) {
        //if(head == NULL)    return head;
        ListNode* pre = NULL;
        ListNode* cur = head;
        ListNode* p = NULL;
        while(cur != NULL){
            p = cur -> next;
            cur->next = pre;
            pre = cur;
            cur = p;
        }
        return pre;
    }
};
解法二 递归法
  • 待完善
解法三 使用栈
  • 待完善

4.两两交换链表中的节点(24)

题目

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

img

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

思路及题解

解法一 模拟(迭代)
  • 这道题正常迭代即可
  • 关键是画图,明白修改 next 指针的过程和操作的先后顺序,否则容易乱
  • 考虑好各种边界条件
  • 注意清理内存
  • 可以使用虚拟头节点,这样操作更加方便
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        //使用了两个指针 pre 和 cur
        ListNode* pre = head;
        if(!pre)    return head;
        ListNode* cur = head->next;
        if(!cur)    return head;
        //切换头指针,实际上是指向原来第二个元素
        head = cur;
        while(true){
            ListNode* p = new ListNode(0,pre);
            pre->next = cur -> next;
            cur->next = p->next;
            pre = pre->next;
            //在这里要判断一次退出条件
            if(pre == nullptr || pre->next == nullptr)  break;
            cur->next->next = pre->next;
            cur = pre->next;
            //每次遍历要释放内存
            delete p;
            p = nullptr;
        }
        return head;
    }
};

以下是优化过的版本:

//这个版本使用了虚拟头节点,并且切换next的操作更加简洁
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        ListNode* result = dummyHead->next;
        delete dummyHead;
        return result;
    }
};
解法二 递归
  • 待完善

5.删除链表的倒数第 N 个节点(19)

题目

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

img

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

**进阶:**你能尝试使用一趟扫描实现吗?

思路及题解

解法一 双指针
  • 双指针法的典型应用,右指针(快指针)先向右移动 n 位,左指针(慢指针)再和其同时移动,直到右指针指向末尾,删除左指针指向元素即可
  • 注意使用虚拟头节点就可以统一逻辑!一定要习惯使用虚拟头节点!!!
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* left = head;
        ListNode* right = head;
        //移动右指针
        for (int i = 0; i < n; i++)
            right = right->next;
        //没有使用虚拟头节点,需要处理特殊情况
        if (right == nullptr)
            return head->next;
        while (right->next) {
            left = left->next;
            right = right->next;
        }
        //释放内存
        ListNode* temp = left->next;
        left->next = temp->next;
        delete temp;
        temp = nullptr;
        return head;
    }
};

用虚拟头节点的优化版:

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        //学会这种遍历方式!!用上面解放的for有点蠢!
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete tmp;
        
        return dummyHead->next;
    }
};

6.链表相交(160)

题目

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null

图示两个链表在节点 c1 开始相交**:**

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

示例 1:

img

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

img

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

img

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 0 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

**进阶:**你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

思路及题解

解法一 双指针
  • 创建指针curA遍历A链表,curB遍历B链表,当 curA == curB 时,指针指向的节点即为相交节点(因为按next指针继续往前走,后面的指针都相同)

  • 我们可以先将两个链表遍历一遍,得到两个链表的长度,再计算出长度差值,让较短的链表与较长的链表对齐,保证尾部对齐(因为如果相交,他们必然是同一个尾部)。然后两个指针同时向前,返回指向相同节点的指针。如果没有这个指针,返回NULL

    /**
     * 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) {
            ListNode* curA = headA;
            ListNode* curB = headB;
            int countA = 0, countB = 0;
            while (curA) {
                curA = curA->next;
                countA++;
            }
            while (curB) {
                curB = curB->next;
                countB++;
            }
    
            curA = headA;
            curB = headB;
            int sub;
            if (countA >= countB) {
                sub = countA - countB;
                while (sub--) {
                    curA = curA->next;
                }
            } else {
                sub = countB - countA;
                while (sub--) {
                    curB = curB->next;
                }
            }
            while (curA && curB) {
                if (curA == curB)
                    return curA;
                curA = curA->next;
                curB = curB->next;
            }
            return NULL;
        }
    };
    
解法二 哈希表
  • 待完善
解法三 另一种双指针
  • 待完善

7.环形链表(142)

题目

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

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

不允许修改 链表。

示例 1:

img

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

**进阶:**你是否可以使用 O(1) 空间解决此题?

思路及题解

解法一 双指针,环形链表典型解法

解决这道题,实际上要解决的是两个问题

  • 链表是否是环形链表(是否有环)
  • 这个环形链表的入口在哪里

先解决第一个问题:判断一个链表是否是环形链表

如图所示,只需设置一个快指针和一个慢指针。假设快指针每次走两步,慢指针每次走一步,如果有环,那么快慢指针一定会在环里相遇。几个问题如下:

  • 如果没有环,那么快指针会直接指向空
  • 快指针可以在环内绕好几圈,而慢指针还没有进环
  • 慢指针一旦进环,快指针一定在慢指针绕完一圈之前追上它。因为快指针速度是慢指针两倍,快指针绕完一圈,慢指针只绕了半圈
  • 快指针不会越过慢指针。因为等价于每次移动,快指针向慢指针靠近了一个节点的距离。若快指针每次走三步,慢指针每次走两步,每次快指针向慢指针靠近两个节点的距离,那么快指针就可能越过慢指针
  • 当快指针指向的节点等于慢指针指向的节点时,证明链表有环

第二个问题:找到环形链表的入口

作以下数学推导:

假设头节点离环形入口节点的距离为 x ,fast指针与slow指针相遇节点离环形入口节点为 y,剩下的具体为 z ,如图所示

快指针的速度是慢指针的两倍时间相同,慢指针走过的距离的两倍等于快指针走过的距离

又因为快指针可能已经在环里转了很多圈,假设转了 n 圈

得式子:2(x + y) = x + n(y + z)

化简得:x = n(y + z) - 2y(要计算出 x 的长度,得到入口位置)

由于“-2y”在图中意义不明,我们将这个式子再次化简,将括号拆开,得: x = (n-1)(y+z) + z

注意 n 大于等于 1,因为快指针至少走一圈才能遇见慢指针

当 n=1 时,有 x=z,即如果在相遇节点定义一个指针 index1,在头节点定义一个指针 index2,两个指针同时前进,最后会在环形入口节点处相遇

当 n > 1时,等价于多走了 n -1 个 y+z 的距离,也就是多转了 n -1 圈,最后两个指针还是会在环形入口节点处相遇

当两个指针指向同一个节点时,我们认为这个节点是环形入口节点

/**
 * 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* fast = head;
        ListNode* slow = head;
        //判断是否是环形链表,不是返回NULL
        while (fast && fast->next) {
            fast = fast->next->next;
            slow = slow->next;
            if (fast == slow) {
                //快慢指针相遇,是环形链表,设两个指针,两指针向前,指向同一节点时停止
                ListNode* p = fast;
                ListNode* q = head;
                while (p != q) {
                    p = p->next;
                    q = q->next;
                }
                //p指向同一节点,返回该节点
                return p;
            }
        }
        return NULL;
    }
};
  • 时间复杂度:O(n),快慢指针相遇和两个指针p、q相遇时走的长度均小于链表长度,加起来长度小于2n
  • 空间复杂度:O(1),在链表上原地操作
解法二 哈希表

遍历链表,用哈希表存储每次遇到的 ListNode 的指针(key),value 值为出现的次数,当出现次数大于1时,该节点为环形链表入口。若没有这种节点,返回NULL

/**
 * 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) {
        map<ListNode*,int> map;
        while(head!=NULL){
            map[head]++;
            if(map[head] > 1)   return head;
            head = head->next;
        }
        return NULL;
    }
};
  • 时间复杂度:O(n),遍历了一次链表
  • 空间复杂度:O(n),使用了哈希表,将链表中所有节点都保存进去
解法三 两层遍历(暴力解法)

设置两个指针,一个向前遍历,一个留在原地判断。当两个指针重合,说明这个节点是环形链表入口。当遍历的指针遍历超过10000次节点时(节点的最大数目),停止遍历这个节点,换成下一个节点。

该方法较为耗时

/**
 * 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* p =head;
        ListNode* q =head;
        int count=0;
        while(p!=NULL){
            q = p;
            count = 0;
            while(q!=NULL){
                q = q->next;
                if(q == p)  return p;
                if(count++ > 10000) break;
            }
            if(q == NULL)   return NULL;
            p = p->next;
        }
        return NULL;
    }
};
  • 时间复杂度:O(n2),两层遍历
  • 空间复杂度:O(1)

总结


哈希表

  • 当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

  • 在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示:

    集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
    std::set红黑树有序O(log n)O(log n)
    std::multiset红黑树有序O(logn)O(logn)
    std::unordered_set哈希表无序O(1)O(1)
  • map:

    映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
    std::map红黑树key有序key不可重复key不可修改O(logn)O(logn)
    std::multimap红黑树key有序key可重复key不可修改O(log n)O(log n)
    std::unordered_map哈希表key无序key不可重复key不可修改O(1)O(1)
  • 选取原则(优先级从上到下):

    • 数据范围较小的集合——数组
    • 数据范围较大较零散集合,无序:unordered_set
    • 集合有序:set,元素有重复:multiset
    • 键值对无序:unordered_map
    • 键值对有序:map,元素有重复:multimap

1.有效的字母异位词(242)

题目

给定两个字符串 *s**t* ,编写一个函数来判断 *t* 是否是 *s* 的字母异位词。

**注意:**若 *s**t* 中每个字符出现的次数都相同,则称 *s**t* 互为字母异位词。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true

示例 2:

输入: s = "rat", t = "car"
输出: false

提示:

  • 1 <= s.length, t.length <= 5 * 104
  • st 仅包含小写字母

进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

思路及题解

解法一 排序

将两个字符串排序,比较两个字符串是否相等,相等即为字母异位词,反之不是。注意先判断字符串长度是否相等,不相等则一定不是字母异位词

class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.size() != t.size())
            return false;
        sort(s.begin(), s.end());
        sort(t.begin(), t.end());
        return s == t;
    }
};

时间复杂度:比较字符串是否相等O(n),sort内部用快速排序O(n log n),整体复杂度O(n log n)

空间复杂度:快速排序本质是二叉树,需要O(log n)大小空间

解法二 哈希表——map

用两个map,key为字符串的每个字母,value为字母出现的次数,比较每个字母出现的次数是否相等,占用空间较大,不是哈希表法的最佳方案

class Solution {
public:
    bool isAnagram(string s, string t) {
        if (s.size() != t.size())
            return false;
        map<char, int> m1;
        map<char, int> m2;
        for (int i = 0; i < s.size(); i++) {
            m1[s[i]]++;
        }
        for (int i = 0; i < t.size(); i++) {
            m2[t[i]]++;
        }
        for (int i = 0; i < s.size(); i++) {
            if (m1[s[i]] != m2[s[i]])
                return false;
        }
        return true;
    }
};

时间复杂度:O(n),遍历两个字符串和比较字母出现次数都是这个复杂度

空间复杂度:O(n),即两个map的容量O(2n),最坏情况下需要存储两个字符串的所有字母

解法三 哈希表——数组
  • 这里数据的范围较小(只有小写字母,26个),故考虑用数组实现哈希表
  • 用 “ - ‘a’ ” 构建各个字母对数组下标的唯一映射,遍历两个字符串(事先判断长度是否相等,则可将两次遍历合并)。当字符串s中出现一次某字母,个数加一,当t中出现该字母,个数减一,最后再遍历一次数组,若数组中元素全为0,则说明两字符串是字母异位词
class Solution {
public:
    bool isAnagram(string s, string t) {
        //注意初始化是0而不是‘0’,否则最后遍历出来的值是0的ASCII码
        int hash[26] = {0};
        if (s.size() != t.size())
            return false;
        for (int i = 0; i < s.size(); i++) {
            hash[s[i] - 'a']++;
            hash[t[i] - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (hash[i] != 0)
                return false;
        }
        return true;
    }
};

时间复杂度:O(n),遍历字符串

空间复杂度:O(1),数组大小空间


2.两个数组的交集(349)

题目

给定两个数组 nums1nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 1000

思路及题解

解法一 哈希表——unordered_set
  • 注意题目特意说明:输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序

  • 考虑用数组,如果没有限制数据的大小,则不能用数组,因为**如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。**但此题限定了数据的大小在1000以内,故 可以用数组

  • 若不能用数组,则考虑set。这里不用考虑输出的顺序,因此可以用unordered_set,降低查询和增删的时间复杂度

  • 用set而不用multiset:给结果去重

  • 为什么遇到哈希问题不能直接都用set:

    直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。

    不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> set;
        vector<int> output;
        //两次遍历,如果两个字符串对应的字母相同,则加入set中
        for (int i = 0; i < nums1.size(); i++) {
            for (int j = 0; j < nums2.size(); j++) {
                if (nums1[i] == nums2[j])
                    set.insert(nums1[i]);
            }
        }
        //set中的元素转移到vector中
        for (unordered_set<int>::iterator it = set.begin(); it != set.end();
             it++) {
            output.push_back(*it);
        }
        return output;
    }
};
  • 时间复杂度:O(n2),遍历了两次字符串
  • 空间复杂度:O(n)
  • 可以优化的地方:
    1. 两次遍历,时间复杂度太大,可以将nums1的元素先存入一个set中,再用一次循环将nums2的元素与set进行对比
    2. set中的元素转移到vector中,不需要用遍历的方式,直接用迭代器拷贝即可
  • 优化过的代码如下:
class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> result_set; // 存放结果
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        for (int num : nums2) {
            // 用find函数找相同元素,注意没有找到的话find返回的是end迭代器
            if (nums_set.find(num) != nums_set.end()) {
                result_set.insert(num);
            }
        }
        // 转换为vector返回
        return vector<int>(result_set.begin(), result_set.end());
    }
};
  • 时间复杂度:O(n+m),多了vector的复杂度
  • 空间复杂度:O(n)
解法二 哈希表——数组
	class Solution {
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
        unordered_set<int> res;
        int hash[1000] = {0};
        for (int num : nums1) {
            hash[num]++;
        }
        for (int num : nums2) {
            if (hash[num] >= 1) {
                res.insert(num);
            }
        }
        return vector<int>(res.begin(), res.end());
    }
};
  • 时间复杂度:O(n+m)
  • 空间复杂度:O(n)

3.快乐数(202)

题目

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

示例 1:

输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

示例 2:

输入:n = 2
输出:false

提示:

  • 1 <= n <= 231 - 1

思路及题解

解法一 哈希表——set
  • 注意到题目告诉我们 sum 会无限循环,由此联想到判断一个数是否出现过——用哈希表
  • 将每次求和的数值进行判断,如果sum是1,返回true。否则判断其有没有在set出现过**(常用find函数)**,如果出现过,返回false,否则将其插入到set中
  • 注意求一个数字各位和(平方和)的方法
class Solution {
public:
    //求各位和的函数
    int getSum(int n) {
        int sum = 0;
        while (n) {
            sum += (n % 10) * (n % 10);
            n /= 10;
        }
        return sum;
    }
    bool isHappy(int n) {
        unordered_set<int> set;
        while (true) {
            int sum = getSum(n);
            if (sum == 1)
                return true;
            else {
                if (set.find(sum) != set.end())
                    return false;
                else {
                    set.insert(sum);
                    n = sum;
                }
            }
        }
    }
};
  • 时间复杂度:O(log n)
  • 空间复杂度:O(log n)

4.两数之和(1)

题目

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

**进阶:**你可以想出一个时间复杂度小于 O(n2) 的算法吗?

思路及题解

解法一 暴力解法

两个for循环即可,不再赘述

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int size = nums.size();
        for (int i = 0; i < size - 1; i++) {
            for (int j = i + 1; j < size; j++) {
                if (nums[i] + nums[j] == target)
                    return { i, j };
            }
        }
        return {};
    }
};
  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)
解法二 哈希表——map

本题有四个重点:

  • 为什么会想到用哈希表:当要知道遍历到的元素是否之前出现过,考虑用哈希表。本题求两数的和是否为 target ,可以转化为 target 减去 遍历到的数 是否在之前出现过,故想到用哈希表
  • 哈希表为什么用map:为了方便找到之前出现过的数的下标,便于输出,所以用map来存放
  • 本题map是用来存什么的:存放之前遍历过的数
  • map中的key和value用来存什么的:key存放数的数值,value存放数的下标。因为用find函数查找时查找的是数的数值,故数值要放到key中

由于本题对顺序没有要求,故用 unordered_map 即可

遍历各个元素,如果差值在之前出现过,返回。否则存放到 map 中

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> map;
        for (int i = 0; i < nums.size(); i++) {
            unordered_map<int, int>::iterator it = map.find(target - nums[i]);
            if (it == map.end()) {
                map.insert(pair<int, int>(nums[i], i));
            } else {
                return {it->second, i};
            }
        }
        return {};
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

5.四数相加 II(454)

题目

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:

  • n == nums1.length
  • n == nums2.length
  • n == nums3.length
  • n == nums4.length
  • 1 <= n <= 200
  • -228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228

思路及题解

解法一 哈希表——map
  • 暴力解法,4个for循环,时间复杂度为O(n4),时间复杂度极差
  • 考虑将四个数相加拆分成两部分,一次是 a+b 的遍历,一次是 c+d 的遍历,这样时间复杂度就降低到了O(n2)
  • 为什么不是拆分成 a 和 b+c+d 呢?因为遍历 b+c+d 的时间复杂度为O(n3),时间复杂度比两个O(n2)要高
  • 要使 a+b+c+d 的值等于0,则要找到 0-(c+d) 的值是否在 a+b 的遍历中出现过——哈希表
  • 有很多种a、b的组合和是相同的,都可以算作元组中的一部分,因此要统计某个 a+b 的值出现过的次数——用map
  • 本题不涉及顺序,故使用unordered_map
  • count为元组的个数,注意找到符合条件的 a+b 的值是count不是加一,而是加上value即该 a+b 的值出现过的次数,因为每个次数代表一种a和b的组合,都可以组成一个元组
class Solution {
public:
    int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3,
                     vector<int>& nums4) {
        unordered_map<int, int> map;
        int count = 0;
        //循环可以使用增强for循环来优化
        for (int i = 0; i < nums1.size(); i++) {
            for (int j = 0; j < nums1.size(); j++) {
                map[nums1[i] + nums2[j]]++;
            }
        }
        for (int i = 0; i < nums1.size(); i++) {
            for (int j = 0; j < nums1.size(); j++) {
                unordered_map<int, int>::iterator it =
                    map.find(0 - (nums3[i] + nums4[j]));
                if (it != map.end()) {
                    count += it->second;
                }
            }
        }
        return count;
    }
};
  • 时间复杂度:O(n2)
  • 空间复杂度:O(n2),最坏情况下a和b元素均不同,a+b有n2个值

6.赎金信(383)

题目

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:

输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

输入:ransomNote = "aa", magazine = "ab"
输出:false

示例 3:

输入:ransomNote = "aa", magazine = "aab"
输出:true

提示:

  • 1 <= ransomNote.length, magazine.length <= 105
  • ransomNotemagazine 由小写英文字母组成

思路及题解

解法一 哈希表——数组
  • 找magazine里出现的字母是否在ransomNote中出现过——哈希表
  • 数值的范围不大,限定在小写字母范围内——数组
  • 创建一个数组,先遍历ransomNote,通过减去‘a’的操作将字母与数组元素一一映射,出现的字母对应的位置值加一
  • 再遍历magazine,出现的字母对应数值减1,最后再遍历hash数组,若出现大于0的值,说明magazine里缺少ransomNote中的某个字母,返回false
class Solution {
public:
    bool canConstruct(string ransomNote, string magazine) {
        int hash[26] = {0};
        //扣细节,可以先比较ransomNote和magazine的长度,若后者小于前者,可以直接返回false
        for (int i = 0; i < ransomNote.size(); i++) {
            hash[ransomNote[i] - 'a']++;
        }
        for (int i = 0; i < magazine.size(); i++) {
            hash[magazine[i] - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (hash[i] > 0)
                return false;
        }
        return true;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1),数组的大小

7.三数之和(15)

题目

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

**注意:**答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105

思路及题解

解法一 哈希表——set
  • 类似之前的题目四数相加II,可以两次遍历a和b的值,再将0-(a+b)的值与哈希表中的值(事先存入c)做对比,看是否出现过
  • 这样的思路本来没有问题,但复杂在去重的操作极其麻烦,需要考虑的条件非常多,而且去重的过程消耗的时间非常长,这里只把代码随想录里的代码附上
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2
                        && nums[j] == nums[j-1]
                        && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                    continue;
                }
                int c = 0 - (nums[i] + nums[j]);
                if (set.find(c) != set.end()) {
                    result.push_back({nums[i], nums[j], c});
                    set.erase(c);// 三元组元素c去重
                } else {
                    set.insert(nums[j]);
                }
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n2)
  • 空间复杂度:O(n)
解法二 双指针法

  • 实现思路:首先将数组排序,方便进行后续操作
  • 循环遍历 i ,i 即为a,每次遍历再设置两个指针left和right,分别作为b和c,left初始值为 i + 1,right 初始值为 nums.size() -1.
  • 注意如果 nums[ i ] 大于0,则立即结束遍历,因为数组已经排好序,三数之和必定大于0
  • a的去重操作:因为每次遍历时,a为这个值的所有可能三元组均已被加入数组中,所以如果下个a(即下个i)指向的数和上一个数相同,则这次遍历不进行,直接 continue。因为数组已经排好序,所以后面能执行遍历的 i 必定没有执行过
  • 注意a去重时不能将这次要遍历的数和下次要便利的数进行比较,应该是这次要遍历的数与上次遍历过的数进行比较,否则会漏掉这次的遍历
  • 循环条件为 right < left,因为两个指针指向同一个数时,不可能得到符合条件的三元组(三个数互相不是同一个数,即下标不同)
  • 计算 nums[ i ] + nums[ left ] + nums[ right ]的值,如果这个值大于0,说明三数相加的值太大,right需要向左移动一位(设定每次遍历 i 不动,left只能向右移动,right 只能向左移动)。如果小于0,left向右移动一位,直到值为0,将这个三元组加入数组中
  • 加入数组后进行b和c的去重操作,如果 left 的后一位(或right的前一位)数值和本次遍历数值相同,同a去重的道理,所有b(c)为这个值的所有可能三元组均已被加入数组中,故继续向右(向左)移动
  • 注意b和c的去重要在加入过一次三元组后进行,否则会漏掉
class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); i++) {
            if (nums[i] > 0)
                return res;
            if (i > 0 && nums[i] == nums[i - 1])
                continue;
             // 错误去重a方法,将会漏掉-1,-1,2 这种情况
            /*
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            */
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) {
                 // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) {
                    right--;
                } else if (nums[i] + nums[left] + nums[right] < 0) {
                    left++;
                } else {
                    res.push_back({nums[i], nums[left], nums[right]});
                    while (right > left && nums[right] == nums[right - 1])
                        right--;
                    while (right > left && nums[left] == nums[left + 1])
                        left++;
                      // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }
        }
        return res;
    }
};
  • 时间复杂度: O(n^2)
  • 空间复杂度: O(1)

思考

两数之和 就不能使用双指针法,因为两数之和要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。

如果两数之和要求返回的是数值的话,就可以使用双指针法了。


8.四数之和(18)

题目

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

思路及题解

解法一 双指针法
  • 延续三数之和的思路即可

  • 四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。

    那么一样的道理,五数之和、六数之和等等都采用这种解法。(即双指针只能处理两层for循环,剩下的还是要依靠for循环来遍历)

    对于三数之和双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。

  • 这里特别要注意的是剪枝的操作,不能用像三数之和的 nums[ i ] > 0 的写法,因为这里改成了 target ,但也不能写 nums[i] > target ,因为有可能 target 是个负数,nums[ i ]若是比他大的负数,n后面的数是正数,也可能得到 target 值,故剪枝时需要加上条件:nums[ i ] 大于 0,这样nums [ i ] 后面的数也都是正数(已经排好序),相加必然也超过 target 值

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> res;
        sort(nums.begin(), nums.end());
        for (int i = 0; i < nums.size(); i++) {
            // if(nums[i] > target) return res;  不能有这个条件
            //正确剪枝操作
            if (nums[i] > target && nums[i] >= 0)
                //这里 break 掉,统一用最后的 return 返回
                break;
            //去重操作
            if (i > 0 && nums[i] == nums[i - 1])
                continue;
            for (int j = i + 1; j < nums.size(); j++) {
                //也是同样的剪枝操作
                if (nums[i] + nums[j] > target && nums[i] + nums[j] >= 0)
                    break;
                if (j > i + 1 && nums[j] == nums[j - 1])
                    continue;

                int left = j + 1;
                int right = nums.size() - 1;
                while (right > left) {
                    // 注意加long,因为相加的数可能超过 int 的范围!!!!!!
                    if ((long)nums[i] + nums[j] + nums[left] + nums[right] >
                        target)
                        right--;
                    else if ((long)nums[i] + nums[j] + nums[left] +
                                 nums[right] <
                             target)
                        left++;
                    else {
                        res.push_back(
                            {nums[i], nums[j], nums[left], nums[right]});
                        while (right > left && nums[right - 1] == nums[right])
                            right--;
                        while (right > left && nums[left + 1] == nums[left])
                            left++;
                        left++;
                        right--;
                    }
                }
            }
        }
        return res;
    }
};
  • 时间复杂度:O(n3)
  • 空间复杂度:O(1)

  • 34
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值