Leetcode220.存在重復元素 III

題目描述

給定一個整數數組,判斷數組中是否有兩個不同的索引ij,使得nums[i]nums[j]的差的絕對值最大為t,并且ij之間的差的絕對值最大為k。

示例1

輸入: nums = [1,2,3,1], k = 3, t = 0
輸出: true

示例2

輸入: nums = [1,0,1,1], k = 1, t = 2
輸出: true

示例3

輸入: nums = [1,5,9,1,5,9], k = 2, t = 3
輸出: false

題解源自官方:https://leetcode-cn.com/problems/contains-duplicate-iii/solution/cun-zai-zhong-fu-yuan-su-iii-by-leetcode/
方法一(線性搜索)【超時】
思路

將每個元素與它之前的 kk 個元素比較,查看它們的數值之差是不是在 t 以內。

public Boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t){
    for(int i=0; i<nums.length; ++i){
        for(int j=Math.max(i-k, 0); j<i; ++j){
            if(Math.abs(nums[i]-nums[j])<=t)
                return true;
        }
    }
    return false;
復雜度分析
  • 時間復雜度:O(nmin(k,n)) 每次搜索都將花費 O(min(k,n)) 的時間,需要注意的是一次搜索中我們最多比較 n 次,哪怕 k 比 n 大。
  • 空間復雜度:O(1) 額外開辟的空間為常數個。
方法二 (二叉搜索樹)【通過】
思路
  • 如果窗口中維護的元素是有序的,只需要用二分搜索檢查條件二是否是滿足的就可以了。
  • 利用自平衡二叉搜索樹,可以在對數時間內通過插入刪除來對滑動窗口內元素排序。
算法

方法一真正的瓶頸在于檢查第二個條件是否滿足需要掃描滑動窗口中所有的元素。因此我們需要考慮的是有沒有比全掃描更好的方法。

如果窗口內的元素是有序的,那么用兩次二分搜索就可以找到 x+t 和 x?t 這兩個邊界值了。

然而不幸的是,窗口中的元素是無序的。這里有一個初學者非常容易犯的錯誤,那就是將滑動窗口維護成一個有序的數組。雖然在有序數組中搜索只需要花費對數時間,但是為了讓數組保持有序,我們不得不做插入刪除的操作,而這些操作是非常不高效的。想象一下,如果你有一個kk大小的有序數組,當你插入一個新元素x的時候。雖然可以在O(logk)時間內找到這個元素應該插入的位置,但最后還是需要O(k)的時間來將x插入這個有序數組。因為必須得把當前元素應該插入的位置之后的所有元素往后移一位。當你要刪除一個元素的時候也是同樣的道理。在刪除了下標為i的元素之后,還需要把下標i之后的所有元素往前移一位。因此,這種做法并不會比方法一更好。

為了能讓算法的效率得到真正的提升,我們需要引入一個支持插入搜索刪除操作的動態數據結構,那就是自平衡二叉搜索樹自平衡這個詞的意思是,這個樹在隨機進行插入,刪除操作之后,它會自動保證樹的高度最小。為什么一棵樹需要自平衡呢?這是因為在二叉搜索樹上的大部分操作需要花費的時間跟這顆樹的高度直接相關。可以看一下下面這棵嚴重左偏的非平衡二叉搜索樹。

image.png

在上面這棵二叉搜索樹上查找一個元素需要花費 線性 時間復雜度,這跟在鏈表中搜索的速度是一樣的。現在我們來比較一下下面這棵平衡二叉搜索樹。
image.png

假設這棵樹上節點總數為n,一個平衡樹能把高度維持在h=logn。因此這棵樹上支持在O(h)=O(logn)時間內完成插入搜索刪除操作。
下面給出整個算法的偽代碼:

  • 初始化一棵空的二叉搜索樹set
  • 對于每個元素x,遍歷整個數組
    -- 在set上查找大于等于x的最小的數,如果s-x≤t則返回true
    -- 在set上查找小于等于x的最大的數,如果x?g≤t則返回true
    -- 在set中插入x
    -- 如果樹的大小超過了k, 則移除最早加入樹的那個數。
  • 返回false
    我們把大于等于x的最小的數s當做x在 BST 中的后繼節點。同樣的,我們能把小于等于x最大的數g當做x在 BST 中的前繼節點。sg這兩個數是距離x最近的數。因此只需要檢查它們和x的距離就能知道條件二是否滿足了。
public Boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t){
    TreeSet<Integer> set = new TreeSet<>();
    for(int i=0; i<nums.length; ++i){
        //Find the successor of current element
        Integer s = set.ceiling(nums[i]);
        if(s != null && s <= nums[i]+t)
            return true;
        
        //Find the predecessor of current element
        Integer g = set.floor(nums[i]);
        if(g != null && nums[i] <= g + t)
            return true;
        
        set.add(nums[i]);
        if (set.size() > k){
            set.remove(nums[i-k]);
        }
    }
    return false;
}
復雜度分析
  • 時間復雜度:O(nlog(min(n,k)))我們需要遍歷這個n長度的數組。對于每次遍歷,在 BST 中 搜索,插入或者刪除都需要花費O(logmin(k,n))的時間。
  • 空間復雜度:O(min(n,k))空間復雜度由 BST 的大小決定,其大小的上限由kn共同決定。
筆記

當數組中的元素非常大的時候,進行數學運算可能造成溢出。所以可以考慮使用支持大數的數據類型,例如 long。
C++ 中的std::setstd::set::upper_boundstd::set::lower_bound分別等價于 Java 中的TreeSetTreeSet::ceilingTreeSet::floor。Python 標準庫不提供自平衡 BST。

方法三(桶)【通過】
思路

桶排序的啟發,我們可以把桶當做窗口來實現一個線性復雜度的解法。

算法

桶排序是一種把元素分散到不同桶中的排序算法。接著把每個桶再獨立地用不同的排序算法進行排序。
回到這個問題,我們嘗試去解決的最大的問題在于:

1.對于給定的元素x, 在窗口中是否有存在區間 [x?t,x+t] 內的元素?
2.我們能在常量時間內完成以上判斷嘛?

我們不妨把把每個元素當做一個人的生日來考慮一下吧。假設你是班上新來的一位學生,你的生日在 三月 的某一天,你想知道班上是否有人生日跟你生日在t=30天以內。在這里我們先假設每個月都是30天,很明顯,我們只需要檢查所有生日在 二月,三月,四月 的同學就可以了。

之所以能這么做的原因在于,我們知道每個人的生日都屬于一個桶,我們把這個桶稱作月份!每個桶所包含的區間范圍都是t,這能極大的簡化我們的問題。很顯然,任何不在同一個桶或相鄰桶的兩個元素之間的距離一定是大于t的。

我們把上面提到的桶的思想應用到這個問題里面來,我們設計一些桶,讓他們分別包含區間...,[0,t],[t+1,2t+1],...。我們把桶來當做窗口,于是每次我們只需要檢查x所屬的那個桶和相鄰桶中的元素就可以了。終于,我們可以在常量時間解決在窗口中搜索的問題了。

還有一件值得注意的事,這個問題和桶排序的不同之處在于每次我們的桶里只需要包含最多一個元素就可以了,因為如果任意一個桶中包含了兩個元素,那么這也就是意味著這兩個元素是 足夠接近的 了,這時候我們就直接得到答案了。因此,我們只需使用一個標簽為桶序號的散列表就可以了。

public class Solution{
    // Get the ID of the bucket from element value x and bucket width w
    // In Java, `-3 / 5 = 0` and but we need `-3 / 5 = -1`.
    private long getID(long x, long w){
        return x < 0 ? (x + 1) / w - 1 : x / w;  // w, 每個桶包含的范圍
    }

    public boolean containsNearbyAlmostDuplicate(int [] nums, int k, int t){
        if(t<0)
            return false;
        Map<Long, Long> d = new HashMap<>();
        long w = (long) t + 1;
        for(int i=0; i<nums.length; ++i){
            long m = getID(nums[i], w);
            // check if bucket m is empty, each bucket may contain at most one element
            if(d.containsKey(m))
                return true;
            // check the nei***or buckets for almost duplicate
            if (d.containsKey(m - 1) && Math.abs(nums[i] - d.get(m - 1)) < w)
                return true;
            if (d.containsKey(m + 1) && Math.abs(nums[i] - d.get(m + 1)) < w)
                return true;
            // now bucket m is empty and no almost duplicate in nei***or buckets
            d.put(m, (long)nums[i]);
            if (i >= k)
                d.remove(getID(nums[i - k], w));
        }
        return false;
    }
}
復雜度分析
  • 時間復雜度:O(n)
    對于這 n 個元素中的任意一個元素來說,我們最多只需要在散列表中做三次 搜索,一次 插入 和一次 刪除。這些操作是常量時間復雜度的。因此,整個算法的時間復雜度為 O(n)。
  • 空間復雜度:-O(min(n,k))
    需要開辟的額外空間取決了散列表的大小,其大小跟它所包含的元素數量成線性關系。散列表的大小的上限同時由 n 和 k 決定。因此,空間復雜度為 O(min(n,k))。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • 一些概念 數據結構就是研究數據的邏輯結構和物理結構以及它們之間相互關系,并對這種結構定義相應的運算,而且確保經過這...
    Winterfell_Z閱讀 5,882評論 0 13
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,266評論 0 2
  • 概述 排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    蟻前閱讀 5,209評論 0 52
  • 今天早起打卡遲到。 事情的經過是這樣的,一直習慣早起手環鬧鐘叫醒,昨晚手環充電,沒有佩戴,早晨第一次迷糊中醒來,看...
    皮兒米閱讀 518評論 0 0
  • 中國好公民,普通老百姓。時代造就模樣,拓攀與時動。幾曾夢想無限,北闖南拼爭競命運誰掌控?乘風奮挺進,成敗都英雄!呵...
    老楊哥閱讀 169評論 0 0