程式碼隨想錄-day2

2023-03-02 18:01:45

雜湊表

基礎知識

雜湊表和連結串列都是屬於基礎資料結構的一種,都是必須掌握牢靠的知識。

雜湊表是根據關鍵碼的值而直接進行存取的資料結構。

簡單來說就是使用資料得到的雜湊值來作為雜湊表的key用於獲取資料。

用於求雜湊值的的函數被我們稱為雜湊函數,通過雜湊函數我們可以把資料對映到我們的雜湊表上。

顯然,在我們計算雜湊值的時候我們不可避免的會出現兩個資料計算出的雜湊值相同。這種不同資料,雜湊值相同的情況我們稱為雜湊碰撞

在出現了雜湊碰撞之後,我們應該如何解決呢?

在這裡我們介紹兩種解決的方案:

  • 拉鍊法
  • 線性探測法(開放定址法)

拉鍊法:

如圖所示,我們假設在雜湊值為0的地方產生了衝突,於是我們可以在雜湊值為0的位置存一個指標,指向一個連結串列。我們使用連結串列儲存相同雜湊的資料。

線性探測法:

線性探測法要求雜湊表 實際的大小 > 資料的大小 。因為線性探測法處理衝突的方法是從雜湊值的位置開始一直尋找一個不衝突的位置來儲存資料。

如圖,當a和b雜湊值相同的時候,我們從a的位置開始往下找到第一個沒有儲存資料的位置存放b(如圖就是位置3)。

實現方式

雜湊表在實際中的常見實現方式有三種:

  • 陣列
  • set (集合)
  • map (對映)

我們可以根據題目的具體要求和實現難易,選擇合適的實現方式。

總的來說,當我們要以O(1)的時間複雜度判斷一個數是否在一個集合中存在時,我們就可以考慮使用雜湊表。

242.有效的字母異位詞

題意:給定兩個字串 s 和 t ,編寫一個函數來判斷 t 是否是 s 的字母異位詞。

範例 1: 輸入: s = "anagram", t = "nagaram" 輸出: true

範例 2: 輸入: s = "rat", t = "car" 輸出: false

說明: 你可以假設字串只包含小寫字母。

思路:題目的要求可以抽象為兩個字串是否為完全相同的字母(數量也相同)組成,根據我們剛剛總結的雜湊表的作用,我們可以使用雜湊表存一個字串中包含的所有字母及其個數。

這樣在遍歷另一個字串的時候,我們就可以快速的判斷該字元是否在前一個字串中出現。

本題可以使用map實現,但是使用陣列會更快且更簡單。

我們申請一個陣列大小為26位,現在我們就可以按下標0-25 對應 a-z,陣列存的值為該字母出現的次數。

程式碼:

class Solution {
    public boolean isAnagram(String s, String t) {
        int[] map = new int[26];

        if (s.length() !=  t.length()) return false;

        for (int i = 0; i < s.length(); i ++ ) {
            map[s.charAt(i) - 'a'] ++; 
            map[t.charAt(i) - 'a'] --;
        }

        for (int cnt : map) {
            if (cnt != 0) return false; // 如果一個字母在s和t中出現的次數不一樣,那麼存的值一定不為0
        }
        return true;
    }
}

349. 兩個陣列的交集

題意:給定兩個陣列,編寫一個函數來計算它們的交集。

範例:

說明: 輸出結果中的每個元素一定是唯一的。 我們可以不考慮輸出結果的順序。

思路

根據題意,我們不能看出我們需要儲存一個字串的字元,然後判斷另一個字元是否在該集合中,如果在我們就把它加入到答案裡,注意每個元素是唯一的,這就說明答案中元素是沒有重複的。

根據沒有重複的這個性質,我們可以使用set來實現雜湊表,因為set可以去重。

程式碼

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        Set<Integer> set = new HashSet<>();
        Set<Integer> set1 = new HashSet<>();

        for (int x : nums1) set.add(x);
        for (int x : nums2) set1.add(x);

        return intersectionFunc(set, set1);
    }

    public int[] intersectionFunc(Set<Integer> set, Set<Integer> set1) {
        if (set.size() < set1.size()) return intersectionFunc(set1, set);

        Set<Integer> res = new HashSet<>();
        for (int x : set1) {
            if (set.contains(x)) res.add(x);
        }

        return res.stream().mapToInt(x -> x).toArray();
    }
}

第202題. 快樂數

題意:編寫一個演演算法來判斷一個數 n 是不是快樂數。

「快樂數」定義為:對於一個正整數,每一次將該數替換為它每個位置上的數位的平方和,然後重複這個過程直到這個數變為 1,也可能是 無限迴圈 但始終變不到 1。如果 可以變為 1,那麼這個數就是快樂數。

如果 n 是快樂數就返回 True ;不是,則返回 False 。

範例:

輸入:19
輸出:true
解釋:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

思路

本題的是不斷迴圈的過程,當中途出現重複值的時候,程式一定會陷入死迴圈,所以此時我們退出迴圈。

那我們怎麼判斷一個值在之前出現過呢,顯而易見我們可以使用雜湊表儲存每一個計算出來的值。

根據思路不難寫出程式碼,另外一個要注意的點就是把數位的每一位加上起來求和的過程。我們可以使用取模運算每次取到數的最後一位,再讓該數減少一位。

程式碼

class Solution {
    public boolean isHappy(int n) {
        Set<Integer> hash = new HashSet<>();

        while (true) {
            int res = getSum(n);
            if (res == 1) return true;
            if (hash.contains(res)) return false;
            else hash.add(res);
            n = res;
        } 

    }


    public int getSum(int n) {
        int sum = 0;
        while (n > 0) {
            sum += Math.pow(n % 10, 2);
            n /= 10; 
        }
        return sum;
    }
}

1. 兩數之和

題意:給定一個整數陣列 nums 和一個目標值 target,請你在該陣列中找出和為目標值的那 兩個 整數,並返回他們的陣列下標。

你可以假設每種輸入只會對應一個答案。但是,陣列中同一個元素不能使用兩遍。

範例:

給定 nums = [2, 7, 11, 15], target = 9

因為 nums[0] + nums[1] = 2 + 7 = 9

所以返回 [0, 1]

思路

根據題意,我們很容易就能想到一個暴力的解法,即兩層迴圈列舉兩個數並計算他們的和是否等於target。但是這樣的時間複雜度為O(n2)

我們該如何優化它呢?因為target的值是固定的,我們假設兩個數滿足的數為x和y,可以得到x + y = target。 當我們固定x的值時,y的值也被固定為y = targer - x

所以我們可以只列舉一個值,然後查詢另一個值是否在陣列中存在即可。

程式碼

class Solution {
    public int[] twoSum(int[] nums, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        int[] ans = new int[2];
        for (int i = 0; i < nums.length; i ++ ) {
            if (map.containsKey(target - nums[i])) {
                ans[0] = map.get(target - nums[i]);
                ans[1] = i;
                break;
            } else {
                map.put(nums[i], i);
            }
        }
        return ans;
    }
}

第454題.四數相加II

題意:給定四個包含整數的陣列列表 A , B , C , D ,計算有多少個元組 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。

為了使問題簡單化,所有的 A, B, C, D 具有相同的長度 N,且 0 ≤ N ≤ 500 。所有整數的範圍在 -2^28 到 2^28 - 1 之間,最終結果不會超過 2^31 - 1 。

思路

本題思路同兩數之和,我們可以先儲存A + B的值, 再列舉B 和 C, 查詢是否存在滿足 0 - A - B的值存在。

因為題目只用滿足的數目,所以我們只需要在 查詢到滿足0 - A - B的值時將ans++即可

本題還有一個值得注意的點,就是在儲存A + B的值的時候我們需要儲存 A 和 B 中 相同和的次數,所以我們選擇使用map來實現雜湊表。

程式碼

class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> hash = new HashMap<>();
        int ans = 0;
        for (int i : nums1)
            for (int j : nums2) {
                if (hash.containsKey(i + j)) {
                    hash.put(i + j, hash.get(i + j) + 1);
                } else {
                    hash.put(i + j, 1);
                }
            }

        for (int i :nums3)
            for (int j : nums4) {
                if (hash.containsKey(0 - i - j)) {
                    ans += hash.get(0 - i - j);
                }
            }

        return ans;
    }
}

383. 贖金信

題意:給定一個贖金信 (ransom) 字串和一個雜誌(magazine)字串,判斷第一個字串 ransom 能不能由第二個字串 magazines 裡面的字元構成。如果可以構成,返回 true ;否則返回 false。

(題目說明:為了不暴露贖金信字跡,要從雜誌上搜尋各個需要的字母,組成單詞來表達意思。雜誌字串中的每個字元只能在贖金信字串中使用一次。)

注意:

你可以假設兩個字串均只含有小寫字母。

canConstruct("a", "b") -> false
canConstruct("aa", "ab") -> false
canConstruct("aa", "aab") -> true

思路

鑑於是存的字母及其出現次數,所以我們採用陣列來實現雜湊表。

我們可以先儲存雜誌上出現的字母及其出現次數。然後列舉贖金信的每一個字元在雜湊表中查詢,如果該字母在雜湊表中為0,即為該字母在雜誌不存在所以返回false。

如果不等於0我們讓該值減減,表示使用了一個雜誌的字母了。

當遍歷完整個字串後還沒有找到一個字母在雜誌中沒有,則返回true。

程式碼

class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        int[] map = new int[26];

        for (int i = 0; i < magazine.length(); i ++ ) {
            map[magazine.charAt(i) - 'a'] ++;
        }

        for (int i = 0; i < ransomNote.length(); i ++ ) {
            if (map[ransomNote.charAt(i) - 'a'] == 0) return false;
            else map[ransomNote.charAt(i) - 'a'] --;
        }

        return true;
    }
}

第15題. 三數之和

題意:給你一個包含 n 個整數的陣列 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出所有滿足條件且不重複的三元組。

注意: 答案中不可以包含重複的三元組。

範例:

給定陣列 nums = [-1, 0, 1, 2, -1, -4],

滿足要求的三元組集合為: [ [-1, 0, 1], [-1, -1, 2] ]

思路

本題可以使用雜湊表來解,但是使用雜湊表時去重的步驟很複雜,而且使用雜湊法解題的時候時間複雜度為O(n2)

所以本題和接下來的四數之和我們都選擇使用雙指標的解法。

本來我們需要列舉三個數,時間複雜度為O(n3), 在我們使用雙指標後可以優化到O(n2)

具體的,我們列舉一個數i,然後令left = i + 1, right = n - 1 (n 為陣列的長度)。注意,在我們使用雙指標操作時我們必須先把陣列排序。

因為陣列排序後是遞增的,所以我們可以得到一下的性質:

  • nums[i] + nums[left] + nums[right] > 0 時,我們可以讓right --,使得結果向等於0靠近。
  • nums[i] + nums[left] + nums[right] < 0 時, 我們可以讓lef ++,使得結果向等於0靠近。

對於這兩種性質我們可以為迴圈剪枝:

  • x + nums[i + 1] + nums[i + 2] > 0時,證明當前的i 加上最小的兩個數也大於0了,又因為陣列遞增,所以i之後同樣不會有解break。
  • x + nums[n - 1] + nums[n - 2] < 0時,證明當前的i 加上最大的兩個值還是小於0的,但是因為i之後的數可能比它大所以直接continue。

我們繼續考慮怎麼去重,我們需要對三個數都去重,我們假設三個數為a,b,c

對於a 即 nums[i]的去重,有一個問題,是判斷 nums[i] 與 nums[i + 1]是否相同,還是判斷 nums[i] 與 nums[i-1] 是否相同。

這裡存在一個陷阱,大家可能覺得這兩種情況不是一樣的嘛,實際上兩者的意義並不同。

我們以 nums[i] 與 nums[i + 1]是否相同進行判斷的時候,nums[i + 1] 對應這 nums[left] 這實際上是保證了三元組中不存在重複元素,和題目要求的不重複的三元組不同。

所以我們在for迴圈的一開始先對a去重:if (i > 0; nums[i] == nums[i - 1]) continue;

對於 b 和 c的去重怎麼考慮呢?

同樣的我們可以參考a的去重模式,但是left 和 right 是一個不斷列舉的值所以我們要在while迴圈裡面對left 和 right 一直判斷,更新

ans.add(Arrays.asList(x, nums[j], nums[k]));
while (j < k && nums[j] == nums[j + 1]) j ++;
while (k > j && nums[k] == nums[k - 1]) k --;
j ++;
k --;

程式碼

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        int n = nums.length;

        for (int i = 0; i < n - 2; i ++ ) {
            int x = nums[i];
            if (i != 0 && nums[i] == nums[i - 1]) continue;
            if (x + nums[i + 1] + nums[i + 2] > 0) break;
            if (x + nums[n - 1] + nums[n - 2] < 0) continue;

            int j = i + 1;
            int k = n - 1;

            while (j < k) {
                int sum = x + nums[j] + nums[k];
                if (sum > 0) k --;
                else if (sum < 0) j ++;
                else {
                    ans.add(Arrays.asList(x, nums[j], nums[k]));
                    while (j < k && nums[j] == nums[j + 1]) j ++;
                    while (k > j && nums[k] == nums[k - 1]) k --;
                    j ++;
                    k --;
                }
            }
        }

        return ans;
    }
}

第18題. 四數之和

題意:給定一個包含 n 個整數的陣列 nums 和一個目標值 target,判斷 nums 中是否存在四個元素 a,b,c 和 d ,使得 a + b + c + d 的值與 target 相等?找出所有滿足條件且不重複的四元組。

注意:

答案中不可以包含重複的四元組。

範例: 給定陣列 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 滿足要求的四元組集合為: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ]

思路

四數之和,和15.三數之和是一個思路,都是使用雙指標法, 基本解法就是在三數之和的基礎上再套一層for迴圈。

但是剪枝的邏輯和三數之和略有不同,因為target不再是固定為0的值,所以我們可以這修改剪枝的條件if (nums[i] > 0 && nums[i] > target) return ans;

程式碼

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length;

        List<List<Integer>> ans = new ArrayList<>();

        for (int i = 0; i < n; i ++ ) {

            if (nums[i] > 0 && nums[i] > target) return ans;
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            for (int j = i + 1; j < n; j ++ ) {
                if (j > i + 1 && nums[j] == nums[j - 1]) continue;
                int left = j + 1;
                int right = n - 1;

                while (left < right) {
                    long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum > target) right --;
                    else if (sum < target) left ++;
                    else {
                        ans.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        while (left < right && nums[left] == nums[left + 1]) left ++;
                        while (right > left && nums[right] == nums[right - 1]) right --;
                        left ++;
                        right --;
                    }
                }
            }
        }

        return ans;
    }
}