技術圖文:字典技術在求解算法題中的應用

背景

前段時間,在知識星球立了一個Flag,這是總結Leetcode刷題的第二篇圖文。

在總結這篇圖文的時候,順便總結了 C# 中Dictionary類的實現,大家可以參考一下:

理論部分

C# 中字典的常用方法

對於 C# 中的 Dictionary類 相信大家都不陌生,這是一個 Collection(集合) 類型,可以通過 Key/Value (鍵值對) 的形式來存放數據;該類最大的優點就是它查找元素的時間複雜度接近 O(1),實際項目中常被用來做一些數據的本地緩存,提升整體效率。

常用方法如下:

  • public Dictionary(); -> 構造函數
  • public Dictionary(int capacity); -> 構造函數
  • public void Add(TKey key, TValue value); -> 將指定的鍵和值添加到字典中。
  • public bool Remove(TKey key); -> 將帶有指定鍵的值移除。
  • public void Clear(); -> 將所有鍵和值從字典中移除。
  • public bool ContainsKey(TKey key); -> 確定是否包含指定鍵。
  • public bool ContainsValue(TValue value); -> 確定否包含特定值。
  • public TValue this[TKey key] { get; set; } -> 獲取或設置與指定的鍵關聯的值。
  • public KeyCollection Keys { get; } -> 獲得鍵的集合。
  • public ValueCollection Values { get; } -> 獲得值的集合。
public static void DicSample()
{
    Dictionary<string, string> dic = new Dictionary<string, string>();
    try
    {
        if (dic.ContainsKey("Item1") == false)
        {
            dic.Add("Item1", "ZheJiang");
        }
        if (dic.ContainsKey("Item2") == false)
        {
            dic.Add("Item2", "ShangHai");
        }
        else
        {
            dic["Item2"] = "ShangHai";
        }
        if (dic.ContainsKey("Item3") == false)
        {
            dic.Add("Item3", "BeiJing");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Error: {0}", e.Message);
    }

    if (dic.ContainsKey("Item1"))
    {
        Console.WriteLine("Output: " + dic["Item1"]);
    }

    foreach (string key in dic.Keys)
    {
        Console.WriteLine("Output Key: {0}", key);
    }

    foreach (string value in dic.Values)
    {
        Console.WriteLine("Output Value: {0}", value);
    }
    
    foreach (KeyValuePair<string, string> item in dic)
    {
        Console.WriteLine("Output Key : {0}, Value : {1} ", item.Key, item.Value);
    }
}

// Output: ZheJiang
// Output Key: Item1
// Output Key: Item2
// Output Key: Item3
// Output Value: ZheJiang
// Output Value: ShangHai
// Output Value: BeiJing
// Output Key: Item1, Value: ZheJiang
// Output Key: Item2, Value: ShangHai
// Output Key: Item3, Value: BeiJing

注意:增加鍵值對之前需要判斷是否存在該鍵,如果已經存在該鍵而不判斷,將拋出異常。

Python 中字典的常用方法

Python中的 字典 是無序的 鍵:值(key:value)對集合,在同一個字典之內鍵必須是互不相同的。

  • dict 內部存放的順序和 key 放入的順序是沒有關係的。
  • dict 查找和插入的速度極快,不會隨着 key 的增加而增加,但是需要佔用大量的內存。

字典 定義語法爲 {元素1, 元素2, ..., 元素n}

  • 其中每一個元素是一個「鍵值對」-- 鍵:值 (key:value)
  • 關鍵點是「大括號 {}」,「逗號 ,」和「冒號 :」
  • 大括號 – 把所有元素綁在一起
  • 逗號 – 將每個鍵值對分開
  • 冒號 – 將鍵和值分開

常用方法如下:

  • dict() -> 構造函數。
  • dict(mapping) -> 構造函數。
  • dict(**kwargs) -> 構造函數。
  • dict.keys() -> 返回一個可迭代對象,可以使用 list() 來轉換爲列表,列表爲字典中的所有鍵。
  • dict.values() -> 返回一個迭代器,可以使用 list() 來轉換爲列表,列表爲字典中的所有值。
  • dict.items() -> 以列表返回可遍歷的 (鍵, 值) 元組數組。
  • dict.get(key, default=None) -> 返回指定鍵的值,如果值不在字典中返回默認值。
  • dict.setdefault(key, default=None) -> 和get()方法 類似, 如果鍵不存在於字典中,將會添加鍵並將值設爲默認值。
  • key in dict -> in 操作符用於判斷鍵是否存在於字典中,如果鍵在字典 dict 裏返回true,否則返回false
  • key not in dict -> not in操作符剛好相反,如果鍵在字典 dict 裏返回false,否則返回true
  • dict.pop(key[,default]) -> 刪除字典給定鍵 key 所對應的值,返回值爲被刪除的值。key 值必須給出。若key不存在,則返回 default 值。
  • del dict[key] -> 刪除字典給定鍵 key 所對應的值。
def DicSample(self):
    dic = dict()
    try:
        if "Item1" not in dic:
            dic["Item1"] = "ZheJiang"
        if "Item2" not in dic:
            dic.setdefault("Item2", "ShangHai")
        else:
            dic["Item2"] = "ShangHai"
        dic["Item3"] = "BeiJing"
    except KeyError as error:
        print("Error:{0}".format(str(error)))
        
    if "Item1" in dic:
        print("Output: {0}".format(dic["Item1"]))
        
    for key in dic.keys():
        print("Output Key: {0}".format(key))
        
    for value in dic.values():
        print("Output Value: {0}".format(value))
        
    for key, value in dic.items():
        print("Output Key: {0}, Value: {1}".format(key, value))

# Output: ZheJiang
# Output Key: Item1
# Output Key: Item2
# Output Key: Item3
# Output Value: ZheJiang
# Output Value: ShangHai
# Output Value: BeiJing
# Output Key: Item1, Value: ZheJiang
# Output Key: Item2, Value: ShangHai
# Output Key: Item3, Value: BeiJing

應用部分

題目1:兩數之和

  • 題號:1
  • 難度:簡單
  • https://leetcode-cn.com/problems/two-sum/

給定一個整數數組 nums 和一個目標值 target,請你在該數組中找出和爲目標值的那 兩個整數,並返回他們的數組下標。

你可以假設每種輸入只會對應一個答案。但是,你不能重複利用這個數組中同樣的元素。

示例1:

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

因爲 nums[0] + nums[1] = 2 + 7 = 9,所以返回 [0, 1]

示例2:

給定 nums = [230, 863, 916, 585, 981, 404, 316, 785, 88, 12, 70, 435, 384, 778, 887, 755, 740, 337, 86, 92, 325, 422, 815, 650, 920, 125, 277, 336, 221, 847, 168, 23, 677, 61, 400, 136, 874, 363, 394, 199, 863, 997, 794, 587, 124, 321, 212, 957, 764, 173, 314, 422, 927, 783, 930, 282, 306, 506, 44, 926, 691, 568, 68, 730, 933, 737, 531, 180, 414, 751, 28, 546, 60, 371, 493, 370, 527, 387, 43, 541, 13, 457, 328, 227, 652, 365, 430, 803, 59, 858, 538, 427, 583, 368, 375, 173, 809, 896, 370, 789], target = 542

因爲 nums[28] + nums[45] = 221 + 321 = 542,所以返回 [28, 45]

思路:利用字典的方式

把字典當作一個存儲容器,key 存儲已經出現的數字,value 存儲數組的下標。

C# 語言

  • 執行結果:通過
  • 執行用時:280 ms, 在所有 C# 提交中擊敗了 96.53% 的用戶
  • 內存消耗:31.1 MB, 在所有 C# 提交中擊敗了 6.89% 的用戶
public class Solution 
{
    public int[] TwoSum(int[] nums, int target) 
    {
        int[] result = new int[2];
        Dictionary<int, int> dic = new Dictionary<int, int>();
        for (int i = 0; i < nums.Length; i++)
        {
            int find = target - nums[i];
            if (dic.ContainsKey(find))
            {
                result[0] = dic[find];
                result[1] = i;
                break;
            }
            if (dic.ContainsKey(nums[i]) == false)
                dic.Add(nums[i], i);
        }
        return result;    
    }
}  

Python 語言

  • 執行結果:通過
  • 執行用時:52 ms, 在所有 Python3 提交中擊敗了 86.77% 的用戶
  • 內存消耗:15.1 MB, 在所有 Python3 提交中擊敗了 7.35% 的用戶
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        result = list()
        dic = dict()
        for index, val in enumerate(nums):
            find = target - val
            if find in dic is not None:
                result = [dic[find], index]
                break
            else:
                dic[val] = index

        return result

題目2:只出現一次的數字 II

  • 題號:137
  • 難度:中等
  • https://leetcode-cn.com/problems/single-number-ii/

給定一個 非空 整數數組,除了某個元素只出現一次以外,其餘每個元素均出現了三次。找出那個只出現了一次的元素。

說明:

你的算法應該具有線性時間複雜度。 你可以不使用額外空間來實現嗎?

示例 1:

輸入: [2,2,3,2]
輸出: 3

示例 2:

輸入: [0,1,0,1,0,1,99]
輸出: 99

思路:利用字典的方式

把字典當作一個存儲容器,key 存儲數組中的數字,value 存儲該數字出現的頻數。

C# 語言

  • 執行結果:通過
  • 執行用時:112 ms, 在所有 C# 提交中擊敗了 91.53% 的用戶
  • 內存消耗:25.4 MB, 在所有 C# 提交中擊敗了 100.00% 的用戶
public class Solution
{
    public int SingleNumber(int[] nums)
    {
        Dictionary<int, int> dict = new Dictionary<int, int>();
        for (int i = 0; i < nums.Length; i++)
        {
            if (dict.ContainsKey(nums[i]))
            {
                dict[nums[i]]++;
            }
            else
            {
                dict.Add(nums[i], 1);
            }
        }
        return dict.Single(a => a.Value == 1).Key;
    }
}

Python 語言

  • 執行結果:通過
  • 執行用時:40 ms, 在所有 Python3 提交中擊敗了 89.20% 的用戶
  • 內存消耗:15.1 MB, 在所有 Python3 提交中擊敗了 25.00% 的用戶
class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        dic = dict()
        for num in nums:
            if num in dic:
                dic[num] += 1
            else:
                dic[num] = 1

        for k, v in dic.items():
            if v == 1:
                return k
        return -1

題目3:羅馬數字轉整數

  • 題號:13
  • 難度:簡單
  • https://leetcode-cn.com/problems/roman-to-integer/

羅馬數字包含以下七種字符: I, V, X, L,C,DM

字符          數值
I             1
V             5
X             10
L             50
C             100
D             500
M             1000

例如, 羅馬數字 2 寫做II,即爲兩個並列的 1。12 寫做XII,即爲X + II。 27 寫做XXVII, 即爲XX + V + II

通常情況下,羅馬數字中小的數字在大的數字的右邊。但也存在特例,例如 4 不寫做IIII,而是IV。數字 1 在數字 5 的左邊,所表示的數等於大數 5 減小數 1 得到的數值 4 。同樣地,數字 9 表示爲IX。這個特殊的規則只適用於以下六種情況:

I 可以放在 V (5) 和 X (10) 的左邊,來表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左邊,來表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左邊,來表示 400 和 900。

給定一個羅馬數字,將其轉換成整數。輸入確保在 1 到 3999 的範圍內。

示例 1:

輸入:"III"
輸出: 3

示例 2:

輸入: "IV"
輸出: 4

示例 3:

輸入: "IX"
輸出: 9

示例 4:

輸入: "LVIII"
輸出: 58
解釋: L = 50, V= 5, III = 3.

示例 5:

輸入: "MCMXCIV"
輸出: 1994
解釋: M = 1000, CM = 900, XC = 90, IV = 4.

思路:利用字典的方式

把字典當作一個存儲容器,key 存儲羅馬字符的所有組合,value 存儲該組合代表的值。

每次取一個字符,判斷這個字符之後是否還有字符。如果有,則判斷這兩個字符是否在字典中,如果存在則取值。否則,按照一個字符去取值即可。

C# 語言

  • 執行結果:通過
  • 執行用時:120 ms, 在所有 C# 提交中擊敗了 42.16% 的用戶
  • 內存消耗:25.8 MB, 在所有 C# 提交中擊敗了 5.27% 的用戶
public class Solution
{
    public int RomanToInt(string s)
    {
        Dictionary<string, int> dic = new Dictionary<string, int>();
        dic.Add("I", 1);
        dic.Add("II", 2);
        dic.Add("IV", 4);
        dic.Add("IX", 9);
        dic.Add("X", 10);
        dic.Add("XL", 40);
        dic.Add("XC", 90);
        dic.Add("C", 100);
        dic.Add("CD", 400);
        dic.Add("CM", 900);
        dic.Add("V", 5);
        dic.Add("L", 50);
        dic.Add("D", 500);
        dic.Add("M", 1000);

        int result = 0;
        int count = s.Length;
        int i = 0;
        while (i < count)
        {
            char c = s[i];
            if (i + 1 < count && dic.ContainsKey(s.Substring(i, 2)))
            {
                result += dic[s.Substring(i, 2)];
                i += 2;
            }
            else
            {
                result += dic[c.ToString()];
                i += 1;
            }
        }
        return result;
    }
}

Python 語言

  • 執行結果:通過
  • 執行用時:72 ms, 在所有 Python3 提交中擊敗了 24.93% 的用戶
  • 內存消耗:13.5 MB, 在所有 Python3 提交中擊敗了 5.05% 的用戶
class Solution:
    def romanToInt(self, s: str) -> int:
        dic = {"I": 1, "II": 2, "IV": 4, "IX": 9, "X": 10, "XL": 40, "XC": 90,
               "C": 100, "CD": 400, "CM": 900, "V": 5,
               "L": 50, "D": 500, "M": 1000}
        result = 0
        count = len(s)
        i = 0
        while i < count:
            c = s[i]
            if i + 1 < count and s[i:i + 2] in dic:
                result += dic[s[i:i + 2]]
                i += 2
            else:
                result += dic[c]
                i += 1
        return result

題目4:LRU緩存機制

  • 題號:146
  • 難度:中等
  • https://leetcode-cn.com/problems/lru-cache/

運用你所掌握的數據結構,設計和實現一個 LRU (最近最少使用) 緩存機制。它應該支持以下操作: 獲取數據 get 和 寫入數據 put 。

獲取數據 get(key) - 如果密鑰 (key) 存在於緩存中,則獲取密鑰的值(總是正數),否則返回 -1。

寫入數據 put(key, value) - 如果密鑰不存在,則寫入其數據值。當緩存容量達到上限時,它應該在寫入新數據之前刪除最近最少使用的數據值,從而爲新的數據值留出空間。

進階:

你是否可以在 O(1) 時間複雜度內完成這兩種操作?

示例:

LRUCache cache = new LRUCache( 2 /* 緩存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1);       // 返回  1
cache.put(3, 3);    // 該操作會使得密鑰 2 作廢
cache.get(2);       // 返回 -1 (未找到)
cache.put(4, 4);    // 該操作會使得密鑰 1 作廢
cache.get(1);       // 返回 -1 (未找到)
cache.get(3);       // 返回  3
cache.get(4);       // 返回  4

思路:利用 字典 + 單鏈表 的方式

計算機的緩存容量有限,如果緩存滿了就要刪除一些內容,給新內容騰位置。但問題是,刪除哪些內容呢?我們肯定希望刪掉哪些沒什麼用的緩存,而把有用的數據繼續留在緩存裏,方便之後繼續使用。那麼,什麼樣的數據,我們判定爲「有用的」的數據呢?

LRU 緩存淘汰算法就是一種常用策略。LRU 的全稱是 Least Recently Used,也就是說我們認爲最近使用過的數據應該是是「有用的」,很久都沒用過的數據應該是無用的,內存滿了就優先刪那些很久沒用過的數據。

把字典當作一個存儲容器,由於字典是無序的,即 dict 內部存放的順序和 key 放入的順序是沒有關係的,所以需要一個 list 來輔助排序。

C# 語言

  • 狀態:通過
  • 18 / 18 個通過測試用例
  • 執行用時: 392 ms, 在所有 C# 提交中擊敗了 76.56% 的用戶
  • 內存消耗: 47.9 MB, 在所有 C# 提交中擊敗了 20.00% 的用戶
public class LRUCache
{
    private readonly List<int> _keys;
    private readonly Dictionary<int, int> _dict;


    public LRUCache(int capacity)
    {
        _keys = new List<int>(capacity);
        _dict = new Dictionary<int, int>(capacity);
    }

    public int Get(int key)
    {
        if (_dict.ContainsKey(key))
        {
            _keys.Remove(key);
            _keys.Add(key);
            return _dict[key];
        }
        return -1;
    }

    public void Put(int key, int value)
    {
        if (_dict.ContainsKey(key))
        {
            _dict.Remove(key);
            _keys.Remove(key);
        }
        else if (_keys.Count == _keys.Capacity)
        {
            _dict.Remove(_keys[0]);
            _keys.RemoveAt(0);
        }
        _keys.Add(key);
        _dict.Add(key, value);
    }
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.Get(key);
 * obj.Put(key,value);
 */

Python 語言

  • 執行結果:通過
  • 執行用時:628 ms, 在所有 Python3 提交中擊敗了 12.15% 的用戶
  • 內存消耗:22 MB, 在所有 Python3 提交中擊敗了 65.38% 的用戶
class LRUCache:

    def __init__(self, capacity: int):
        self._capacity = capacity
        self._dict = dict()
        self._keys = list()

    def get(self, key: int) -> int:
        if key in self._dict:
            self._keys.remove(key)
            self._keys.append(key)
            return self._dict[key]
        return -1

    def put(self, key: int, value: int) -> None:
        if key in self._dict:
            self._dict.pop(key)
            self._keys.remove(key)
        elif len(self._keys) == self._capacity:
            self._dict.pop(self._keys[0])
            self._keys.remove(self._keys[0])
        self._keys.append(key)
        self._dict[key] = value

# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)

注意,這兩行代碼不能顛倒順序,否則dict中就不會存在_keys[0]了。

self._dict.pop(self._keys[0])
self._keys.remove(self._keys[0])

總結

本篇圖文總結了字典的概念,以及 C# 和 Python語言對這種常用數據結構類型的封裝。並以四道Leetcode習題舉例說明字典作爲一種容器的具體應用。好了,今天就到這裏吧!Flag進度爲40%,See You!


往期活動

LSGO軟件技術團隊會定期開展提升編程技能的刻意練習活動,希望大家能夠參與進來一起刻意練習,一起學習進步!


我是 終身學習者“老馬”,一個長期踐行“結伴式學習”理念的 中年大叔

我崇尚分享,渴望成長,於2010年創立了“LSGO軟件技術團隊”,並加入了國內著名的開源組織“Datawhale”,也是“Dre@mtech”、“智能機器人研究中心”和“大數據與哲學社會科學實驗室”的一員。

願我們一起學習,一起進步,相互陪伴,共同成長。

後臺回覆「搜搜搜」,隨機獲取電子資源!
歡迎關注,請掃描二維碼:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章