Find the Duplicate Number
Her biri 1 ile n arasında olan n+1 tam sayıdan oluşan bir nums dizisi alırsın. Tam olarak bir değer, belki birçok kez olmak üzere, birden fazla kez görünür ve bu değeri döndürürsün.
nums dizisini değiştirmeden ve yalnızca sabit miktarda ek bellek kullanarak çöz.
Fonksiyon
- numsinteger-array
- n+1 tam sayı, her biri 1 ile n arasında
- Döndürürinteger
- birden fazla görünen değer
Kısıtlar
1 ≤ n ≤ 104nums.length == n+11 ≤ nums[i] ≤ n- Tam olarak bir değer iki veya daha fazla kez görünür; diğer her değer en fazla bir kez görünür.
Örnekler
- Girdi
- nums = [2, 5, 1, 3, 5, 4]
- Çıktı
- 5
- Açıklama
- Burada
n5'tir ve 5, 1 ve 4. konumlarda yer alır; bu nedenle cevap 5'tir. 1'den 5'e kadar olan diğer tüm değerler bir kez görünür.
- Girdi
- nums = [4, 2, 4, 1, 4]
- Çıktı
- 4
- Açıklama
- 4, 0, 2 ve 4 konumlarında olmak üzere üç kez görünürken 3 hiç görünmüyor. Bir tekrar, eksik birkaç değerin yerini alabilir; bu yüzden cevap 4'tür.
Gönderirken +17 gizli test
Ek soru
Değerler üzerinde ikili arama, her iki kuralı da O(n log n) zamanda tutar. Bunları O(n) zamanda tutabilir misin?
İpuçları
Tek tek açın. Her biri biraz daha fazlasını gösterir.
Her değer 1 ile
narasındadır ve dizinin 0'dann'e kadar konumları vardır. Dolayısıyla her değer aynı zamanda geçerli bir konumdur. 0 konumundan başlayın,nums[0]konumuna, ardından o değerin belirttiği konuma ve böyle devam ederek atlayın. Bu yürüyüşte ne olması gerekir?Yürüyüş hiç durmaz ve ziyaret edilecek yalnızca
n+1konum vardır, bu yüzden bir döngüye girer. Döngüye girdiği konuma iki farklı konumdan ulaşılır ve her ikisi de değer olarak o konumu tutar.Döngünün başlangıcını bulmak için 0 konumundan iki işaretçi kullan: biri her turda bir kez, diğeri iki kez ilerlesin; aynı konuma gelene kadar devam et. Ardından birini 0 konumuna geri gönder ve ikisini de her seferinde bir adım ilerlet. Başlangıç noktasında buluşurlar; cevap budur.
Çözüm
Bir hash kümesi veya sıralama tekrarlanan değeri hemen bulur, ancak ikisi de kuralları ihlal eder: küme her değer için bellek gerektirir, sıralama ise nums dizisini değiştirir. Çözüm sayılarda. Her değer 1 ile n arasındadır, dolayısıyla dizide geçerli bir konumdur. Her değeri başka bir konuma giden bir bağlantı olarak okuyun; 0. konumdan başlayıp bağlantıları takip etmek, giriş noktası yinelenen değer olan bir döngüde sonlanır. Floyd'un hızlı ve yavaş işaretçileri bu giriş noktasını iki tamsayıyla bulur.
Her çifti karşılaştır
Doğru, ama en büyük testlerde bitmiyor
Sezgi
Tekrarlanan değer en az iki konumda bulunur: i < j. Her konumu, kendisinden sonraki her konumla karşılaştır; eşit değerleri içeren ilk çift yanıtı verir. İlk örnekte 1. konumda 5 bulunur ve 2. konumdan itibaren yapılan tarama, 4. konumda başka bir 5 bulur.
Bu, her iki kuralı da korur: hiçbir şey yazılmaz ve kullanılan tek bellek iki döngü sayacıdır. Çiftleri karşılaştırdığı için yavaştır. n+1 = 10.001 değer ve her iki kopya da sona yakın olduğunda, yaklaşık 5 × 10^7 çift kontrol eder.
Algoritma
- 0'dan sona kadar her
ikonumu için: ikonumundan sonraki herjkonumu içinnums[i]ilenums[j]değerlerini karşılaştır.- İlk eşleşmede
nums[i]değerini döndür.
def findDuplicate(nums):
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] == nums[j]:
return nums[i]
return -1 # unreachable: the input always holds a repeatDeğer üzerinde ikili arama
Sezgi
Konumları değil, değer aralığını araştırın. Bir kesme değeri m seçin ve nums içindeki kaç öğenin en fazla m olduğunu sayın.
Tekrarlanan d değeri m'den büyükse, 1 ile m arasındaki her değer en fazla bir kez görünür; bu nedenle sayı en fazla m'dir. d en fazla m ise, m'den büyük her değer en fazla bir kez görünür; bu nedenle en fazla n-m öğe m'den büyüktür ve en az m+1 öğe en fazla m'dir. Dolayısıyla "count > m" testi, d'den küçük her m için yanlıştır ve d'den itibaren doğrudur. İkili arama, doğru olduğu ilk m değerini bulur; bu da d'dir.
İkinci örnekte n 4'tür. m = 2 için 2 ve 1 öğeleri, 2 sayısını verir; bu 2'den fazla değildir, dolayısıyla yanıt 2'den büyüktür. m = 3 için sayı yine 2'dir, dolayısıyla yanıt 4'tür. Her turda dizinin tamamı bir kez okunur ve aralık yarıya indirilir; bu nedenle iş miktarı O(n log n)'dir: 10.001 değer üzerinde yaklaşık 14 geçiş.
Algoritma
lowdeğerini 1,highdeğerini isenumsdizisinin uzunluğunun bir eksiği olannolarak ayarla.low < higholduğu sürece,middeğerini ikisinin tam ortası olarak al.numsiçindeki en fazlamiddeğerine sahip öğeleri say.- Sayı
middeğerinden büyüksehigh=midolarak ayarla; aksi takdirdelow=mid+1olarak ayarla. lowdeğerini döndür.
def findDuplicate(nums):
low, high = 1, len(nums) - 1
while low < high:
mid = (low + high) // 2
# How many values fall in 1..mid?
count = 0
for x in nums:
if x <= mid:
count += 1
if count > mid:
high = mid # 1..mid holds more values than it has room for
else:
low = mid + 1 # the repeat is above mid
return lowDeğer bağlantılarında Floyd döngü tespiti
Sezgi
Diziyi bağlantılar olarak oku: i konumu, nums[i] konumuna işaret eder. 0'dan n'ye kadar her konumun tam olarak bir çıkış bağlantısı vardır ve her bağlantı 1 ile n arasındaki bir konuma ulaşır. İlk örnekte bağlantılar 0 → 2, 1 → 5, 2 → 1, 3 → 3, 4 → 5 ve 5 → 4 şeklindedir.
0 konumundan başla ve bağlantıları takip et. Her konumun bir bağlantısı olduğundan bu yürüyüş asla duramaz ve yalnızca n+1 konum olduğundan daha önce gördüğü bir konuma geri dönmek zorundadır. O andan itibaren sonsuza kadar döngüde ilerler. Yol, ρ harfi biçiminde, bir kuyruk ve ardından bir döngüden oluşur. İlk örnekte yürüyüş 0, 2, 1, 5, 4, 5, 4 ve böyle devam eder: kuyruk 0, 2, 1; döngü ise 5, 4'tür. 3 konumu kendisine bağlantı verir ama yürüyüş oraya hiç ulaşmaz; bunun bir zararı yoktur.
Döngünün giriş noktası yinelenen değerdir. Yürüyüş 5'e farklı yerlerden iki kez girer: bir kez kuyruğun sonundan (1. konum, çünkü nums[1] değeri 5'tir) ve bir kez döngünün sonundan (4. konum, çünkü nums[4] değeri 5'tir). İki farklı konum 5 değerini içerdiğinden 5 yinelenir. Kuyrukta her zaman 0 konumu bulunur; çünkü hiçbir değer 0 değildir ve hiçbir bağlantı 0'a geri dönmez. Bu nedenle giriş noktasına her zaman bu iki farklı yoldan ulaşılır. Tam olarak bir değer yinelendiğinden giriş noktası bu değerdir.
Şimdi bağlantılı listedeki döngü tespitinde olduğu gibi iki işaretçiyle giriş noktasını bul. 1. aşamada slow her turda bir bağlantıyı, fast ise iki bağlantıyı takip eder; ikisi döngünün bir yerinde aynı konumda durana kadar devam eder. İlk örnekte 4'te buluşurlar. 2. aşamada slow'u 0'a geri koy, fast'i olduğu yerde bırak ve ikisini de her turda bir bağlantı ilerlet. Giriş noktasında buluşurlar.
2. aşamanın neden işe yaradığını açıklayalım: kuyruğun giriş noktasına ulaşması için T bağlantı gerektiğini ve döngünün C konumu olduğunu varsayalım. İşaretçiler buluştuğunda slow s adım, fast ise 2s adım atmıştır. İkisi de aynı noktada durduğuna göre fast'in attığı fazladan s adım, döngünün tam tur sayısına karşılık gelir. T adım daha sonra slow, 0'dan giriş noktasına ulaşır; fast ise fazladan turları bir şeyi değiştirmediğinden, 0'dan başlayan bir yürüyüşün s+T adım sonundaki konumunda durur. Bu, giriş noktasına ulaşmak için T adım ve ardından s adım, yani tam tur sayısı kadar adımdır; dolayısıyla fast de giriş noktasında olur. Daha önce buluşamazlar; çünkü slow hâlâ kuyruktayken fast döngüden hiç çıkmaz. İlk örnekte slow 2, 1, 5 konumlarından geçerken fast 5, 4, 5 konumlarından geçer ve T = 3 adım sonra 5'te buluşurlar.
Her aşama O(n) adım sürer, kullanılan tek bellek iki konumdur ve nums üzerine hiçbir zaman yazılmaz.
Algoritma
- Her
ikonumununums[i]konumuna bağlanan bir düğüm olarak ele al ve her iki işaretçiyi de 0 konumundan başlat. - 1. Aşama: Eşit olana kadar
slowişaretçisininums[slow]konumuna,fastişaretçisini isenums[nums[fast]]konumuna ilerlet. - 2. Aşama:
slowişaretçisini yeniden 0 konumuna ayarla. - Eşit olana kadar her iki işaretçiyi de her seferinde bir bağlantı ilerlet:
slowişaretçisininums[slow]konumuna,fastişaretçisini isenums[fast]konumuna. - Bu konumu döndür: yinelenen değer budur.
def findDuplicate(nums):
# Treat each index i as a node with one link, to nums[i].
# Phase 1: slow moves one link, fast moves two, until they meet in the cycle.
slow = fast = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# Phase 2: restart one pointer at index 0 and move both one link at a time.
# They meet at the cycle's entrance, the index two positions link to.
slow = 0
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
Tuzaklar ve uç durumlar
Çoğu yanlış yanıt, konumlarla değerlerin karıştırılmasından veya Floyd yönteminin bir aşama erken durdurulmasından kaynaklanır.
- 1. aşamadaki buluşma noktasını döndürmek. Bu, döngü üzerindeki herhangi bir konumdur; giriş olmak zorunda değildir. İlk örnekte işaretçiler 4'te buluşur, ancak yanıt 5'tir.
- İlk hareketten önce
slow == fastkoşulunu kontrol etmek. İkisi de 0'dan başladığı için döngü hemen sona erer. Önce hareket edin, sonra karşılaştırın veya onları bir ve iki bağlantı ileriden başlatın. - Yürüyüşe 0 konumundan başka bir yerde başlamak. Hiçbir değer 0 olmadığından hiçbir bağlantı 0 konumunu göstermez; kuyruğun olmasını garanti eden de budur. Başka bir konumdan başlamak, sizi dışarıdan ulaşmanın mümkün olmadığı bir döngüye sokabilir; ilk örnekteki 3. konum gibi, buradaki giriş hiçbir şeyi kanıtlamaz.
- Tekrar eden değerin tam olarak iki kez geçtiğini varsaymak. Toplam hilesi, toplam eksi
1 + 2 + ... + n, ikinci örnekte 15 eksi 10 = 5 sonucunu verir, ancak yanıt 4'tür. Aynı durum XOR hileleri için de geçerlidir. - Değerler yerine konumlar üzerinde ikili arama yapmak veya
count >= midkoşulunu sınamak. 1 ilemarasındaki hiçbir değer tekrarlanmıyor ve eksik değilse,m'den küçük veya eşit değerlerin sayısı tam olarakmolur; bu yüzden iki tarafı yalnızca>ayırır. nums[x]değerini negatife çevirerek veya değerleri yerlerine takas ederek ziyaret edilen değerleri işaretlemek. Her iki yöntem de işe yarar, ancak ikisi de diziyi değiştirir ve görev bunu yasaklar.
Sıkça sorulan sorular4
Find the Duplicate Number algoritmasının zaman karmaşıklığı nedir?
Floyd'un döngü tespiti, O(n) zamanda ve O(1) ek bellekle çalışır: iki aşamasının her biri en fazla n bağlantının birkaç katı kadar ilerler. Değerler üzerinde ikili arama O(n log n) zaman ve O(1) bellek alır. Her ikili karşılaştırmak O(n²) zaman alır.
Floyd'un döngü tespiti yinelenen sayıyı neden bulur?
Her değeri, bulunduğu konumdan gösterdiği konuma giden bir bağlantı olarak okursanız, 0 konumundan başlayan gezinme bir döngüde sona ermek zorundadır; çünkü hiç durmaz ve gidebileceği yalnızca n+1 konum vardır. Döngüye girdiği konuma kuyruktaki ve döngüdeki iki farklı konumdan ulaşılır; dolayısıyla iki girdi bu değeri içerir. Floyd yöntemi, iki işaretçi kullanarak bir döngünün girişini bulur; böylece yinelenen değeri bulur.
Neden bir karma kümesi kullanmıyor veya diziyi sıralamıyorsun?
Her ikisi de yanıtı O(n) veya O(n log n) zamanda bulur ve gerçek bir programda ikisi de uygun olurdu. Görev, bunları bilerek yasaklıyor: bir hash kümesi O(n) ek bellek kullanır ve sıralama ya nums dizisini değiştirir ya da tam bir kopya gerektirir. Seni döngü yaklaşımına yönelten şey bu kısıtlamalardır.
Neden toplam formülü Yinelenen Sayıyı Bulma problemi için işe yaramıyor?
Dizideki toplamdan 1 + 2 + ... + n çıkarıldığında, yalnızca tekrar eden değer tam olarak iki kez ve diğer tüm değerler bir kez geçtiğinde yinelenen değer elde edilir. Burada tekrar eden değer birçok kez geçebilir ve eksik değerlerin yerini alabilir. [4, 2, 4, 1, 4] dizisinde fark 15 eksi 10 = 5'tir; bu değer dizide bile yoktur.
Benzer problemler
Aynı fikirleri kullanan problemler. İki üçünü çözmek bir kalıbı kalıcı hale getirir.
Python
def findDuplicate(nums):
# Kodu buraya yazınDurum 1
Durum 2
Girdi
nums = [2, 5, 1, 3, 5, 4]
Beklenen
5