LeetCode 브러시 질문(ACM 모드)-03 해시 테이블

참조: 코드 생각

  • 참고: 각 LeetCode 질문은 로컬에서 직접 실행할 수 있는 ACM 코드 모드를 사용하며 파란색 글꼴은 질문의 하이퍼링크입니다.

0. 해시 테이블의 이론적 근거

0.1 해시 테이블

  • 해시 테이블( Hash table, Hash table이라고도 함 )은 키 코드 의 값에 따라 직접 액세스되는 데이터 구조 입니다.일반적으로 배열은 해시 테이블이며 해시 테이블의 키 코드는 인덱스입니다. 배열의 첨자. 를 입력 한 다음 첨자를 통해 배열의 요소에 직접 액세스합니다.

여기에 이미지 설명 삽입

  • 그렇다면 해시 테이블은 어떤 문제를 해결할 수 있습니까? 일반적으로 해시 테이블은 요소가 세트에 나타나는지 여부를 빠르게 결정하는 데 사용됩니다.
    • 예를 들어, 이 학교에 이름이 있는지 확인합니다. 열거의 시간 복잡도는 O(n)인 반면 해시 테이블은 O(1)만 필요합니다. 초기화만 사용하여 이 학교의 학생 이름을 해시 테이블에 저장하면 인덱스 를 통해 직접 알 수 있습니다. 이 학교 동급생인가요 ?

0.2 해시 함수

  • 해시 함수, 학생의 이름을 해시 테이블의 인덱스에 직접 매핑한 다음 인덱스 첨자를 쿼리하여 학생이 이 학교에 있는지 빠르게 알 수 있습니다.

  • 해쉬함수는 아래 그림과 같다 이름은 hashCode 를 통해 값으로 변환된다 일반적으로 해쉬코드는 특정 인코딩 방식을 통해 다른 데이터 형식을 다른 값으로 변환할 수 있으므로 학생의 이름이 인덱스 번호에 매핑된다 해시 테이블에.

여기에 이미지 설명 삽입

  • hashCode로 얻은 값이 해시 테이블의 tableSize 크기보다 큰 경우 어떻게 해야 하나요?

    • 매핑된 인덱스 값이 해시 테이블에 있는지 확인하기 위해 값에 모듈로 작업이 다시 수행되어 학생 이름이 해시 테이블에 매핑될 수 있도록 합니다.
  • 이 시점에서 다시 질문이 옵니다. 학생 수가 해시 테이블의 크기보다 크면 어떻게 됩니까? 이때, 해시함수가 균등하게 계산되더라도 여러 학생의 이름이 동시에 해시테이블의 동일한 인덱스 첨자 위치에 매핑되는 것은 불가피하며, 이를 해시 충돌이라고 한다.

0.3 해시 충돌

  • 그림과 같이 Xiao Li와 Xiao Wang은 모두 인덱스 첨자 1의 위치에 매핑됩니다. 이러한 현상을 해시 충돌이라고 합니다.

여기에 이미지 설명 삽입

0.3.1 해시 충돌 솔루션: 지퍼 방식 및 선형 검출 방식
  • 지퍼 방식
    • Xiao Li와 Xiao Wang은 인덱스 1에서 충돌이 발생하고 충돌하는 요소는 연결 목록에 저장되므로 인덱스를 통해 찾을 수 있습니다.
    • 사실 지퍼 방식은 해쉬 테이블의 적절한 크기를 선택 하여 배열의 빈 값으로 인해 많은 메모리를 낭비하지 않고, 연결 목록 때문에 조회에 너무 많은 시간을 낭비하지 않을 것입니다. 너무 깁니다(데이터 크기는 dataSize, 해시 테이블은 tableSize의 크기).
      여기에 이미지 설명 삽입
  • 선형 프로빙
    • 선형 감지 방법을 사용하는 경우 tableSize > dataSize인지 확인하십시오. 충돌 문제를 해결하려면 해시 테이블의 공석에 의존해야 합니다. 예를 들어 Xiao Li가 충돌 위치에 있으면 공석을 찾아 Xiao Wang의 정보를 배치합니다 . 따라서 tableSize는 dataSize보다 커야 합니다. 그렇지 않으면 해시 테이블에 충돌하는 데이터를 저장할 공간이 없습니다. 그림이 보여주듯이
      여기에 이미지 설명 삽입

0.4 세 가지 일반적인 해시 구조

  • 배열, 세트(컬렉션), 맵(매핑)

  • C++에서 set 및 map은 각각 다음 세 가지 데이터 구조를 제공하며 기본 구현과 장단점은 다음 표에 나와 있습니다.

    • std::unordered_set의 기본 구현은 해시 테이블이고, std::set 및 std::multiset의 기본 구현은 균형 잡힌 이진 검색 트리인 레드-블랙 트리이므로 키 값이 정렬됩니다. , 하지만 키는 수정할 수 없으며 키 값을 변경하면 트리 전체가 혼란스러워 추가하거나 삭제할 수만 있습니다.
      여기에 이미지 설명 삽입

    • std::unordered_map의 기본 구현은 해시 테이블이고 std::map 및 std::multimap의 기본 구현은 레드-블랙 트리입니다. 마찬가지로 std::map 및 std::multimap의 키도 정렬됩니다(이 질문은 종종 기본 언어 컨테이너의 이해를 검사하기 위한 인터뷰 질문으로 사용됨).
      여기에 이미지 설명 삽입

  • 해쉬 문제 해결을 위해 set을 사용할 경우 unordered_set을 먼저 사용하는 것이 질의, 덧셈, 삭제 효율이 최적이므로 set을 순서대로 사용해야 한다면 set을 사용한다. 맵은 키 값 데이터 구조이지만 맵의 키에는 제한이 있지만 값에는 제한이 없습니다. 키의 저장 방법은 레드-블랙 트리를 사용하여 구현되기 때문입니다 .

요약하다

  • std::set 및 std::multiset의 기본 구현은 레드-블랙 트리이고 std::set 및 std::multiset은 인덱싱 및 저장에 레드-블랙 트리를 사용하지만 여전히 해싱 방법, 즉 키 및 값을 사용합니다. . 따라서 이러한 데이터 구조를 사용하여 매핑 문제를 해결하는 것을 여전히 해싱이라고 하며 맵은 동일합니다.
  • 요소가 컬렉션에 있는지 여부를 빠르게 판단하려면 해싱을 고려해야 합니다. 그러나 해시 방식은 빠른 검색을 위해 데이터를 저장하기 위해 추가 배열 세트 또는 맵이 사용되기 때문에 시간을 위한 공간을 희생하기도 합니다.

1. 유효한 애너그램

242. 유효한 애너그램
두 개의 문자열 s와 t가 주어지면 t가 s의 애너그램인지 확인하는 함수를 작성하세요. 참고: s와 t의 각 문자 발생 횟수가 같으면 s와 t는 서로의 애너그램이라고 합니다.

  • 예제 1
    입력: head = [1,2,6,3,4,5,6], val = 6 출력: [1,2,3,4,5]
  • 예시 2
    입력: s = "쥐", t = "자동차"
    출력: 거짓

  • 1 <= s.length, t.length <= 5 * 104
    문자열 s와 t에는 소문자만 포함되어 있다고 가정할 수 있습니다.

1.1 아이디어

  • 배열은 실제로 간단한 해시 테이블이며 이 질문의 문자열에는 소문자만 있으므로 문자열 s의 문자 발생 횟수를 기록하도록 배열을 정의 할 수 있습니다 . 배열을 얼마나 크게 정의해야 합니까? 크기가 26인 배열 레코드를 정의하고 0으로 초기화합니다 . a~z의 ASCII도 26개의 연속 값이기 때문입니다. 예를 들어 문자열 s = "aee", t = "eae"를 판단하면 애니메이션은 다음과 같습니다.
    여기에 이미지 설명 삽입

1.1.1 문자열 s에서 문자의 발생 횟수를 기록하는 배열 레코드 정의

  • a~z의 ASCII 문자는 26개의 연속된 값이므로 문자 a는 첨자 0에 매핑되고 해당 문자 z는 매핑되기 때문에 배열, 즉 해시 테이블의 인덱스 첨자에 ​​매핑되어야 합니다. 아래첨자로 25
  • 문자열 s를 다시 순회할 때 s[i] - 'a'가 위치한 요소에 대해 +1 연산만 수행하면 되고 문자 a의 ASCII를 기억할 필요 없이 상대를 요청하면 됩니다. 값. 이러한 방식으로 문자열 s의 문자 발생 수를 계산할 수 있습니다.

1.1.2 이 문자가 문자열 t에 나타나는지 확인

  • 마찬가지로 문자열 t를 순회할 때 t에 나타나는 문자 매핑 해시 테이블의 인덱스에 있는 값에 -1 연산을 수행합니다.

1.1.3 0이 아닌 레코드 배열 확인

  • 문자열 s와 t에 더 많은 문자가 있거나 더 적은 문자가 있는 문자열을 나타내면 false를 반환합니다.

1.1.4 레코드 배열의 모든 요소가 0인 경우

  • 문자열 s와 t는 애너그램이며 true를 반환한다고 설명합니다.

1.2 코드 구현

// 时间复杂度为:O(n)
// 空间度复杂度:O(1)
#include <iostream>
#include <string>
using namespace std;

class Solution {
    
    
public:
    bool isAnagram(string s, string t) {
    
    
        int record[26] = {
    
    0}; // 用来记录每个字符出现的次数,数组初始化为 0
        // 将字符减去'a'得到一个相对数值,然后将该相对数值作为 record 数组的下标
        // 将对应元素加 1,表示该字符出现了 1 次
        for (int i = 0; i < s.size(); i++) {
    
    
            // 并不需要记住字符 a 的ASCII,只要求出一个相对数值就可以
            record[s[i] - 'a']++;
        }
        // 方法同上,只不过将对应元素减 1,表示该字符出现了 1 次
        for (int i = 0; i < t.size(); i++) {
    
    
            record[t[i] - 'a']--;
        }
        // 遍历 record 数组,如果存在元素不为 0
            // 说明该元素对应的字符在字符串 s 和 t 中出现的次数不一致
            // 因此 s 和 t 不是字母异位词,return false
        for (int i = 0; i < 26; i++) {
    
    
            if (record[i] != 0) {
    
    
                return false;
            }
        }
        // record 数组所有元素都为零 0,说明字符串 s 和 t 是字母异位词
        return true;
    }
};

int main() {
    
    
    string s = "anagram";
    string t = "nagaram";
    Solution solution;
    cout << solution.isAnagram(s, t) << endl;
    return 0;
}

2. 두 배열의 교차점

349. 두 배열의 교집합
두 배열 nums1과 nums2가 주어지면 교집합을 반환합니다. 출력 결과의 각 요소는 출력 결과의 순서와 관계없이 고유해야 합니다.

  • 예제 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

2.1 아이디어

  • 해시 값이 상대적 으로 작고, 특히 분산되어 있고 범위가 매우 큰 경우 배열을 사용하면 공간이 많이 낭비됩니다. 이 때 다른 구조 집합을 사용해야 합니다.

    • std::set 및 std::multiset의 기본 구현은 모두 red-black 트리이고 std::unordered_set의 기본 구현은 해시 테이블입니다.unordered_set을 사용하는 것이 읽기 및 쓰기에 가장 효율적이며 필요가 없습니다. 데이터를 정렬하고 허용하지 마십시오. 데이터가 반복되므로 unordered_set을 선택하십시오.
      여기에 이미지 설명 삽입
  • 하지만 set을 직접 사용하는 것은 배열보다 더 많은 공간을 차지할 뿐만 아니라 배열보다 느립니다 .Set은 값을 키에 매핑하기 위해 해시 계산을 수행해야 합니다. 모두 1000 이내

2.2 코드 구현

  • unordered_set을 해시 테이블로 사용
// 时间复杂度: O(mn)
// 空间复杂度: O(n)
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
    
    
public:
    vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
    
    
        // 使用了无序集合来存储结果集 result_set,保证了结果集中不会有重复元素
        unordered_set<int> result_set; 
        // nums_set 使用了 nums1 容器中的元素来初始化
            // 包含了 nums1 中所有不重复的整数元素,且顺序不确定
        unordered_set<int> nums_set(nums1.begin(), nums1.end());
        // 范围 for 循环:依次取出 nums2 中的每个元素并将其赋值给循环变量 num
            // 判断它是否在 nums_set 中出现过,若是,则将它插入到 result_set 中
        for (int num : nums2) {
    
    
            // nums_set.find(num) 表示在 nums_set 集合容器中查找是否存在值为 num 的元素
            // 如果存在,则返回该元素的迭代器;否则,返回 nums_set.end(),表示未找到该元素
            // 因此 nums_set.find(num) != nums_set.end() 表示可以找到
            if (nums_set.find(num) != nums_set.end()) {
    
    
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};

int main() {
    
    
    vector<int> nums1 = {
    
    1, 2, 2, 1};
    vector<int> nums2 = {
    
    2, 2};
    
    Solution solution;
    vector<int> result = solution.intersection(nums1, nums2);
    for (int num : result) {
    
    
        cout << num << " ";
    }
    cout << endl;
    
    return 0;
}
  • 배열을 해시 테이블로 사용
// 时间复杂度: O(m + n),其中 m 和 n 分别为两个输入数组的长度
// 空间复杂度: O(m + n),因为最坏情况下,两个输入数组没有任何公共元素,此时结果集大小为 0,result_set 占用空间为 O(m+n)
#include <iostream>
#include <vector>
#include <unordered_set>

using namespace std;

class Solution {
    
    
public:
    vector<int> intersection(vector<int> &nums1, vector<int> &nums2) {
    
    
        // 使用了无序集合来存储结果集 result_set,保证了结果集中不会有重复元素
        unordered_set<int> result_set;
        int hash[1005] = {
    
    0};
        // 遍历 nums1 数组,将其中的每个元素作为下标
        // 在 hash 数组相应位置上标记为 1,表示该元素存在于 nums1 中
        for (int num : nums1) {
    
    
            hash[num] = 1;
        }
        // 遍历 nums2 数组中每个元素 num,判断其在 hash 数组中对应位置上的值是否为 1
        // 若是,则说明该元素同时存在于 nums1 和 nums2 中,并插入到 result_set 集合
        for (int num : nums2) {
    
    
            if (hash[num] == 1) {
    
    
                result_set.insert(num);
            }
        }
        return vector<int>(result_set.begin(), result_set.end());
    }
};

int main(int argc, char *argv[]) {
    
    
    vector<int> nums1 = {
    
    1, 2, 2, 1};
    vector<int> nums2 = {
    
    2, 2};

    Solution solution;
    vector<int> result = solution.intersection(nums1, nums2);
    for (int num : result) {
    
    
        cout << num << " ";
    }
    cout << endl;

    return 0;
}

3. 해피넘버

202. 행복한 숫자
숫자 n이 행복한 숫자인지 판단하는 알고리즘을 작성하세요.

  • 행복수의 정의 양의 정수
    는 각 위치에 있는 숫자의 제곱의 합으로 숫자를 대치하고
    숫자가 1이 될 때까지 이 과정을 반복하거나 무한 루프가 될 수 있지만 절대 1로 변하지 않습니다
    . 이 프로세스 결과가 1이면 이 숫자는 행복 숫자입니다.
    n이 행복 숫자이면 true를 반환하고 그렇지 않으면 false를 반환합니다.
  • 예제 1
    입력: n = 19
    출력: 참
    설명:
    1 2 + 9 2 = 82 1^2 + 9^2 = 8212+92=82
    8 2 + 2 2 = 68 8^2 + 2^2 = 6882+22=68
    6 2 + 8 2 = 100 6^2 + 8^2 = 10062+82=100
    1 2 + 0 2 + 0 2 = 1 1^2 + 0^2 + 0^2 = 112+02+02=1
  • 예제 2
    입력: n = 2
    출력: false
  • 提示
    1 < = n < = 2 31 − 1 1 <= n <= 2^{31} - 11<=N<=231-1

3.1 아이디어

  • 제목에 무한루프가 될 것이라고 하는데, 즉 합산 과정에서 합이 반복적으로 나타나게 되므로 이 질문은 해쉬법을 이용하여 합이 반복되는지 여부를 판단하고 , 반복되면 false를 반환하고, 그렇지 않으면 false를 리턴한다. 합계가 판단될 때까지 sum = 1을 찾습니다. 반복 발생이 unordered_set을 사용할 수 있는지 여부

3.2 코드 구현

// 时间复杂度: O(logn)
// 空间复杂度: O(logn)
#include <iostream>
#include <unordered_set>

using namespace std;

class Solution {
    
    
public:
    // 计算 n 每个位上数字的平方和。具体实现:
        // 通过 while 循环不断将 n 除以 10 取余数
        // 然后计算这个余数的平方并加到 sum 中,继续将 n 除以 10
        // 直到 n 为 0 时停止,这样最终就能得到这个数每个位上数字的平方和
    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; // 用来记录每次计算结果 sum,避免重复数字出现
        while (1) {
    
    
            int sum = getSum(n);
            // 如果等于 1,则说明这个数字是快乐数
            if (sum == 1) {
    
    
                return true;
            }
            // 判断 sum 是否已经在 set 中出现过
            // 如果出现过,则表明陷入了循环而不是得到了 1
            if (set.find(sum) != set.end()) {
    
    
                return false;
            } else {
    
    
                set.insert(sum);
            }
            n = sum; // 将计算结果 sum 赋值给变量 n,以便继续进行下一轮计算
        }
    }
};

int main(int argc, char *argv[]) {
    
    
    Solution solution;
    cout << solution.isHappy(19) << endl; // 输出 1
    cout << solution.isHappy(2) << endl; // 输出 0
    
    return 0;
}

4. 두 숫자의 합

1. 두 숫자의 합
정수 배열 nums와 목표 정수 목표값을 정의하고, 합이 목표값 목표값인 두 정수를 배열에서 찾아 배열 첨자를 반환합니다 . 각 입력에 대해 하나의 답변만 있다고 가정할 수 있습니다. 그러나 배열의 동일한 요소가 답변에 반복적으로 나타날 수 없습니다. 원하는 순서로 답변을 반환할 수 있습니다.

  • 예제 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
    유효한 답은 하나만 있을 것입니다.

4.1 아이디어

  • 1. 해싱을 사용하는 경우

    • 요소가 나타났는지 또는 요소가 컬렉션에 있는지 쿼리해야 하는 경우 먼저 해싱을 생각해야 합니다. 이 질문은 순회된 요소를 저장하기 위한 컬렉션이 필요하며, 배열을 순회하여 이 컬렉션에 요소가 나타나는지 여부를 쿼리할 때 해시 메서드 사용을 고려해야 합니다.
  • 2. 해시 테이블에 맵을 사용하는 이유

    • 이 질문은 요소가 순회되었는지 여부를 알아야 할 뿐만 아니라 요소에 해당하는 첨자도 알고 있기 때문에 저장하는 키 값 구조, 요소를 저장하는 키 및 첨자를 저장하는 값을 사용해야 합니다. 의 요소이므로 map 을 사용하는 것이 적절하며 , 질문에는 this The order of keys 가 필요하지 않으므로 std::unordered_map 을 선택하는 것이 더 효율적입니다.

    이 질문에서 해싱을 위해 배열 또는 세트 사용의 제한

    • 배열의 크기는 제한적이며 요소가 적고 해시 값이 너무 크면 메모리 공간이 낭비됩니다.
    • set은 컬렉션이고 그 안에 있는 요소는 키만 될 수 있으며 이 질문은 y의 존재 여부를 판단해야 할 뿐만 아니라 x와 y의 첨자를 반환해야 하므로 y의 첨자 위치도 기록해야 합니다. 쓸 수 없다
  • 3. 이 질문에서 사용된 지도는 무엇입니까?

    • 맵은 배열을 순회할 때 현재 요소와 일치하는 요소 및 해당 첨자를 찾기 위해 이전에 순회한 요소 및 해당 첨자를 기록해야 하기 때문에 방문한 요소를 저장하는 데 사용됩니다.
  • 4. 맵의 키와 값은 무엇을 나타냅니까?

    • 요소가 나타나는지 판단하기 위해 이 요소를 키로 사용하므로 배열의 요소를 키로 사용하고 키는 값에 해당하며 값은 첨자를 저장하는 데 사용됩니다.
    • 따라서 맵의 저장 구조는 {key: 데이터 요소, 값: 데이터 요소에 해당하는 첨자} 입니다.
    • 배열을 순회할 때 현재 순회한 요소와 일치하는 값이 있는지 보기 위해 지도를 쿼리하기만 하면 됩니다. 그렇다면 일치하는 쌍을 찾습니다. 그렇지 않으면 현재 순회한 요소를 맵에 저장합니다. 방문한 요소 , 프로세스는 다음과 같습니다

여기에 이미지 설명 삽입

여기에 이미지 설명 삽입

4.2 코드 구현

// 时间复杂度: O(n)
// 空间复杂度: O(n)
#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

class Solution {
    
    
public:
    vector<int> twoSum(vector<int> &nums, int target) {
    
    
        // 定义无序映射(哈希表)map,用于存储每个元素及其下标
        unordered_map<int, int> map;
        // 遍历当前元素,并在 map 中寻找是否有匹配的 key
        for (int i = 0; i < nums.size(); ++i) {
    
    
            // 声明一个迭代器 iter,并指向 map 中等于 (target - nums[i]) 的元素
            auto iter = map.find(target - nums[i]);
            if (iter != map.end()) {
    
    
                return {
    
    iter->second, i}; // 如果找到,则返回这两个元素的下标
            }
            // 如果没找到匹配对,就把访问过的元素和下标加入到map中
            map.insert(pair<int, int>(nums[i], i));
        }
        // 如果遍历完整个数组后仍未找到符合条件的两个整数,则返回一个空的整数向量
        return {
    
    }; 
    }
};

int main(int argc, char *argv[]) {
    
    
    vector<int> nums = {
    
    2, 7, 11, 15};
    int target = 9;

    Solution solution;
    vector<int> result = solution.twoSum(nums, target);
    cout << "[" << result[0] << ", " << result[1] << "]" << endl;
    
    return 0;
}

5. 숫자 4개 더하기 II

454. 4수 덧셈 II는
4개의 정수 배열 A, B, C, D를 제공하며 배열의 길이는 n입니다. 몇 개의 튜플(i, j, k, l)이 만족할 수 있는지 계산하십시오.

  • 0 <= i, j, k, l < n
  • A[i] + B[j] + C[k] + D[l] == 0
  • 예 1
    입력: A = [1,2], B = [-2,-1], C = [-1,2], D = [0,2]
    출력: 2
    설명:
    두 튜플은 다음과 같습니다.
    1 .(0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0 2.
    ( 1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0
  • 예 2
    입력: A = [0], B = [0], C = [0], D = [0]
    출력: 1
  • 提示
    n == A.길이 == B.길이 == C.길이 == D.길이
    1 <= n <= 200
    -228 <= A[i], B[i], C[i], D[ 나] <= 228

5.1 아이디어

  • 이 질문은 해시 방법을 사용하는 고전적인 질문입니다. A[i] + B[j] + C[k] + D[l] = 0을 찾는 한 합계가 다음과 같은 경우를 고려할 필요가 없습니다. 4개의 반복되는 요소 중 0 과 같습니다. 이 문제를 해결하기 위한 단계 :
    • 1. 먼저 unordered_map을 정의하고, 키는 a와 b의 합을 넣고, 값은 a와 b의 합의 발생 횟수를 넣습니다.
    • 2. 큰 A 및 큰 B 배열을 순회하고 두 배열 요소의 합과 합계의 발생 횟수를 세고 맵에 넣습니다.
    • 3. a+b+c+d = 0의 발생 횟수를 세도록 int 변수 개수를 정의합니다.
    • 4. 그런 다음 큰 C 및 큰 D 배열을 순회하여 맵에 0-(c+d)가 나타나는지 찾고 count를 사용하여 맵의 키에 해당하는 값, 즉 발생 횟수를 계산합니다.
    • 5. 마지막으로 통계 값 개수를 반환합니다.

5.2 코드 구현

// 时间复杂度: O(n^2)
// 空间复杂度: O(n^2),最坏情况下 A 和 B 的值各不相同,相加产生的数字个数为 n^2
#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;

class Solution {
    
    
public:
    int fourSumCount(vector<int> &A, vector<int> &B, vector<int> &C, vector<int> &D) {
    
    
        unordered_map<int, int> umap;
        // 循环遍历 A 和 B 数组,将每个元素对的和添加到哈希表中,同时记录它们出现的次数
        for (int a : A) {
    
    
            for (int b : B) {
    
    
                umap[a + b]++; 
            }
        }
        // 遍历 C 和 D 数组,并在哈希表中查找是否存在等于 0-(c+d) 的键值
        int count = 0;
        for (int c : C) {
    
    
            for (int d : D) {
    
    
                if (umap.find(0 - (c + d)) != umap.end()) {
    
    
                    count += umap[0 - (c + d)]; // 如果存在,则将该元素的出现次数加入计数器中
                }
            }
        }
        return count;
    }
};

int main(int argc, char *argv[]) {
    
    
    vector<int> A = {
    
    1, 2};
    vector<int> B = {
    
    -2,-1};
    vector<int> C = {
    
    -1, 2};
    vector<int> D = {
    
    0, 2};

    Solution solution;
    int result = solution.fourSumCount(A, B, C, D);
    cout << result << endl;
    
    return 0;
}

6. 몸값 편지

383. 랜섬노트
ransomNote와 magazine 두 문자열을 주고 ransomNote가 magazine의 문자로 구성될 수 있는지 판단합니다. 가능한 경우 true를 반환하고 그렇지 않으면 false를 반환합니다. 몸값 편지의 손글씨가 노출되지 않도록 잡지에서 필요한 각 편지를 검색하여 의미를 표현하는 단어를 형성해야 합니다. 잡지의 각 캐릭터는 랜섬에서 한 번만 사용할 수 있습니다.

  • 예시 1
    입력: ransomNote = "a", magazine = "b"
    출력: false
  • 예시 2
    입력: ransomNote = "aa", magazine = "ab"
    출력: false
  • 예시 3
    입력: ransomNote = "aa", magazine = "aab"
    출력: true
  • Tip
    1 <= ransomNote.length, magazine.length <= 105
    ransomNote와 magazine은 소문자로만 구성되어 있다고 가정할 수 있습니다.

6.1 아이디어

  • 이 질문은 1차 스트링 랜섬이 2차 스트링 매거진에 있는 캐릭터로 구성될 수 있는지를 판단하는데 여기서 2가지 유의할 점이 있다.
    • 첫 번째 포인트는 "몸값 편지의 손글씨가 노출되지 않도록 잡지 에서 필요한 각 편지를 검색하여 의미를 표현하는 단어를 형성한다"는 것입니다.
    • 두 번째 항목 "두 문자열 모두 소문자만 포함한다고 가정할 수 있습니다"는 소문자만 의미합니다.
  • 소문자만 있기 때문에 space-for-time 해시 전략을 사용할 수 있으며, 길이 26의 배열을 사용하여 잡지에 문자가 나타나는 횟수를 기록한 다음 ransomNote를 사용하여 배열에 모든 문자가 포함되어 있는지 확인합니다. ransomNote에서 요구하는 것은 해싱에 배열을 적용한 것입니다.

이 질문의 경우 맵 은 레드-블랙 트리 또는 해시 테이블을 유지해야 하고 해시 함수도 수행해야 하므로 시간이 많이 걸리는 맵을 사용하는 것이 배열보다 공간 소모가 큽니다 ! 데이터 양이 많으면 차이를 반영할 수 있으므로 배열이 더 간단하고 직접적이며 효과적입니다.

6.2 코드 구현(해시 구현)

// 时间复杂度: O(n)
// 空间复杂度: O(1)
#include <iostream>
#include <string>

using namespace std;

class Solution {
    
    
public:
    bool canConstruct(string ransomNote, string magazine) {
    
    
        int record[26] = {
    
    0};
        // 如果 ransomNote 的长度大于 magazine,则肯定无法由 magazine 组成
        if (ransomNote.size() > magazine.size()) {
    
    
            return false;
        }
        // 遍历 magazine 字符串中的每个字符,将其对应的 record 数组中的元素加一
        // 表示该字符在 magazine 中出现了一次
        for (int i = 0; i < magazine.length(); ++i) {
    
    
            record[magazine[i] - 'a']++;
        }
        // 遍历 ransomNote 字符串中的每个字符,将其对应的 record 数组中的元素减一
        // 表示尝试用该字符来构建 ransomNote
        for (int j = 0; j < ransomNote.length(); ++j) {
    
    
            record[ransomNote[j] - 'a']--;
            // 如果发现某个字符在 ransomNote 中的出现次数超过了在 magazine 中的出现次数
            // 则说明无法由 magazine 构成 ransomNote,返回 false
            if (record[ransomNote[j] - 'a'] < 0) {
    
    
                return false;
            }
        }
        return true;
    }
};

int main() {
    
    
    string ransomNote = "aa";
    string magazine = "aab";

    Solution solution;
    bool result = solution.canConstruct(ransomNote, magazine);
    if (result) {
    
    
        cout << "Ransom note can be constructed from magazine." << endl;
    } else {
    
    
        cout << "Ransom note cannot be constructed from magazine." << endl;
    }
    return 0;
}

7. 세 숫자의 합

15. 세 숫자의 합은
정수 배열 nums를 제공하고 i != j, i != k 및 j != 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] + 숫자[1] + 숫자[2] = (-1) + 0 + 1 = 0.
    숫자[1] + 숫자[2] + 숫자[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
    − 1 0 5 -10^5- 10 _5 <= 숫자[i] <=1 0 5 10^51 05

7.1 아이디어

중복 제거 작업에서 주의해야 할 세부 사항이 많기 때문에 이 주제에 해시 방법을 사용하는 것은 적절하지 않으며, 해시 방법이 for 루프의 두 계층을 사용하는 경우 수행할 수 있는 가지치기 작업은 매우 제한적이지만 시간 복잡도는 O (n 2 ) O(n^2)O ( n2 ), LeetCode에도 전달할 수 있지만 프로그램 실행 시간이 더 깁니다.

  • 여기에 또 다른 해결책이 있습니다: 이중 포인터 방법 이 질문에 대한 이중 포인터 방법은 해시 방법보다 더 효율적입니다.
    • 1. 이 nums 배열을 예로 들어, 먼저 배열을 정렬한 다음 for 루프의 레이어를 갖습니다. i는 아래첨자 0에서 시작하고 동시에 i+1 위치에 왼쪽 아래첨자를 정의하고 아래첨자를 정의합니다. 위치의 배열 끝에서 오른쪽
    • 2. a + b + c = 0이 되도록 배열에서 abc를 찾습니다( 여기서는 a = nums[i], b = nums[left], c = nums[right]와 동일함 ).
    • 3. 다음 좌우 이동은 어떻게 하나요?
      • nums[i] + nums[left] + nums[right] > 0이면 배열이 정렬되어 있기 때문에 이때 세 숫자의 합이 더 크다는 의미이므로 오른쪽 첨자는 왼쪽으로 이동해야 하므로 세 숫자 이하
      • nums[i] + nums[left] + nums[right] < 0 이면 이때 세 수의 합이 작다는 의미이고, left 는 오른쪽으로 이동하여 세 수의 합을 left 까지 크게 만든다는 뜻입니다. 그리고 바로 만나

여기에 이미지 설명 삽입

7.2 코드 구현

// 时间复杂度: O(n^2)
// 空间复杂度: O(1)
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
    
    
public:
    vector<vector<int>> threeSum(vector<int> &nums) {
    
    
        vector<vector<int>> result;
        sort(nums.begin(), nums.end()); // 首先将数组排序
        // 外层循环遍历整个数组,以每个元素作为第一个数(即假设该数在三元组中最小)
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); ++i) {
    
    
            // 排序之后如果第一个元素已经 > 0,那么不可能凑成三元组
            if (nums[i] > 0) {
    
    
                return result;
            }
            // 对 a 去重
            if (i > 0 && nums[i] == nums[i - 1]) {
    
    
                continue;
            }
            int left = i + 1; // left 指向当前元素的下一个位置
            int right = nums.size() - 1; // right 指向数组末尾
            // 内层循环使用双指针技巧,在已排序的数组中查找剩余两个数
            while (right > left) {
    
    
                if (nums[i] + nums[left] + nums[right] > 0) {
    
    
                    right--; // right 下标向左移动才能让三数之和小一些
                } else if (nums[i] + nums[left] + nums[right] < 0) {
    
    
                    left++; // left 下标向右移动才能让三数之和大一些
                } else {
    
    
                    // 找到了符合要求的三元组,则将其加入结果集中
                    result.push_back(vector<int>{
    
    nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后,对 b 和 c 去重
                    while (right > left && nums[right] == nums[right - 1]) {
    
    
                        right--;
                    }
                    while (right > left && nums[left] == nums[left + 1]) {
    
    
                        left++;
                    }
                    // 找到结果后,双指针同时收缩
                    right--;
                    left++;
                }
            }
        }
        return result;
    }
};

int main(int argc, char *argv[]) {
    
    
    vector<int> nums = {
    
    -1, 0, 1, 2, -1, -4};
    Solution solution;
    vector<vector<int>> ans = solution.threeSum(nums);
    cout << "[";
    for (int i = 0; i < ans.size(); ++i) {
    
    
        cout << "[";
        for (int j = 0; j < ans[i].size(); ++j) {
    
    
            cout << ans[i][j];
            if (j != ans[i].size() - 1) {
    
    
                cout << ", ";
            }
        }
        cout << "]";
        if (i != ans.size() - 1) {
    
    
            cout << ", ";
        }
    }
    cout << "]" << endl;

    return 0;
}
// 输出
[[-1, -1, 2], [-1, 0, 1]]

7.3 중복 제거된 논리적 사고

// 错误:把三元组中出现重复元素的情况直接 pass 掉了
    // 例如 {-1, -1, 2} 这组数据,当遍历到第一个 -1 的时候
    // 判断下一个也是 -1,那这组数据就 pass 掉了
    // 题意是不能有重复的三元组,但三元组内的元素是可以重复的
if (nums[i] == nums[i + 1]) {
    
    
    continue;
}
// 正确:这么写就是当前使用 nums[i],判断前一位是不是一样的元素
    // 再看 {-1, -1, 2} 这组数据,当遍历到第一个 -1 的时候,只要前一位没有 -1
    // 那么 {-1, -1, 2} 这组数据一样可以收录到结果集里
if (i > 0 && nums[i] == nums[i - 1]) {
    
    
    continue;
}

8. 네 숫자의 합

18. 네 개의 숫자의 합
n개의 정수와 목표값 목표로 구성된 배열 nums를 제공합니다. 다음 조건을 모두 충족하고 반복되지 않는 4중 [nums[a], nums[b], nums[c], nums[d]]를 찾아서 반환하십시오 (두 개의 4중 요소가 일대일로 대응되는 경우 두 4배는 반복되는 것으로 간주됨):

  • 0 <= a, b, c, d < n
  • a, b, c, d는 서로 다릅니다.
  • 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]]

8.1 아이디어

  • 4개의 수의 합과 3개의 수의 합은 같은 개념으로, 둘 다 이중 포인터 방식을 사용합니다.기본 솔루션은 3개의 수의 합을 기준으로 for 루프의 레이어를 추가하는 것입니다. 그러나 다음과 같이 주의해야 할 몇 가지 세부 사항이 있습니다.
    • nums[k] > target이 반환될 것이라고 판단하지 마십시오. 세 숫자의 합은 nums[i] > 0으로 반환될 수 있습니다. 0은 이미 명확한 숫자이고 네 숫자의 합의 대상은 임의이기 때문입니다. 값. 예: 배열은 [-4, -3, -2, -1]이고 대상은 -10이며 -4 > -10이므로 건너뛸 수 없습니다. 그러나 여전히 가지치기를 할 수 있으며 논리는 nums[i] > target && (nums[i] >=0 || target >= 0)
    • 네 숫자의 합에 대한 이중 포인터 솔루션은 명확한 값으로 for 루프 nums[k] + nums[i]의 두 레이어이며 여전히 이중 포인터로 루프에 왼쪽 및 오른쪽 첨자가 있습니다. nums[k]를 찾으십시오. + 숫자[i] + 숫자[왼쪽] + 숫자[오른쪽] == 목표

8.2 코드 구현

// 时间复杂度: O(n^3)
// 空间复杂度: O(1)
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
    
    
public:
    vector<vector<int>> fourSum(vector<int> &nums, int target) {
    
    
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 剪枝处理
        for (int k = 0; k < nums.size(); ++k) {
    
    
            if (nums[k] > target && nums[k] >= 0) {
    
    
                break; // 这里使用 break,统一通过最后的 return 返回
            }
            // 对 nums[k] 去重
            if (k > 0 && nums[k] == nums[k - 1]) {
    
    
                continue;
            }
            for (int i = k + 1; i < nums.size(); ++i) {
    
    
                // 二级剪枝处理
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
    
    
                    break;
                }
                // 对 nums[i] 去重
                if (i > k + 1 && nums[i] == nums[i - 1]) {
    
    
                    continue;
                }
                int left = i + 1;
                int right = nums.size() - 1;
                while (right > left) {
    
    
                    // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
                    if ((long)nums[k] + nums[i] + nums[left] + nums[right] > target) {
    
    
                        right--;
                    } else if ((long)nums[k] + nums[i] + nums[left] + nums[right] < target) {
    
    
                        left++;
                    } else {
    
    
                        result.push_back(vector<int>{
    
    nums[k], nums[i], nums[left], nums[right]});
                        // 对 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 result;
    }
};

int main(int argc, char *argv[]) {
    
    
    vector<int> nums = {
    
    -1, 0, 1, 2, -1, -4};
    int target = -1;
    Solution solution;
    vector<vector<int>> ans = solution.fourSum(nums, target);
    cout << "[";
    for (int i = 0; i < ans.size(); ++i) {
    
    
        cout << "[";
        for (int j = 0; j < ans[i].size(); ++j) {
    
    
            cout << ans[i][j];
            if (j != ans[i].size() - 1) {
    
    
                cout << ", ";
            }
        }
        cout << "]";
        if (i != ans.size() - 1) {
    
    
            cout << ", ";
        }
    }
    cout << "]" << endl;

    return 0;
}
// 输出结果
[[-4, 0, 1, 2], [-1, -1, 0, 1]]

추천

출처blog.csdn.net/qq_42994487/article/details/130935245