- Diterbitkan pada
Kegilaan LeetCode 41 Saya: Penerokaan Mendalam tentang Teka-teki Positif Hilang Pertama
- Penulis
- Nama
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Masalah "First Missing Positive" (nombor 41) daripada LeetCode telah menjadi kegilaan pengaturcaraan saya yang terbaru. Ia adalah teka-teki yang kelihatan mudah tetapi sebenarnya mencabar, yang telah mencengkam pemikiran saya, mendorong saya untuk meneroka selok-beloknya dan menghasilkan penyelesaian TypeScript yang paling cekap. Tulisan ini bukan sekadar mengenai menguasai temu duga pengaturcaraan (walaupun itu adalah kelebihan yang bagus). Ia adalah mengenai kegembiraan semata-mata menyelesaikan masalah, keseronokan pengoptimuman, dan keindahan elegan kod yang cekap. Dan kerana saya sedikit 'nerd' matematik, kita juga akan menyelami asas matematik penyelesaiannya.
Masalahnya: Serigala Berpakaian Kulit Domba
Masalah ini membentangkan satu tatasusunan integer yang tidak tersusun. Tugas anda: cari integer positif terkecil yang hilang. Nampak mudah? Fikirkan semula. Kekangan masa dan kerumitan ruang mengubah tugas yang kelihatan mudah ini menjadi teka-teki algoritma yang mencabar.
Perjuangan Awal: Perangkap Kekasaran
Percubaan awal saya melibatkan pengisisan (melanggar kekangan masa ) dan set hash (melebihi had ruang ). Jelas sekali, pendekatan yang lebih canggih diperlukan.
Momen Eureka: Transformasi Dalam-Tempat
Pemahaman utama adalah menyedari bahawa saya boleh menggunakan tatasusunan input itu sendiri sebagai jadual hash. Dengan menyusun semula elemen, saya boleh meletakkan nombor pada indeks (jika , di mana ialah panjang tatasusunan). Transformasi dalam-tempat ini membuka jalan kepada penyelesaian yang cekap.
Evolusi Kod: Saga TypeScript
Berikut adalah kronik evolusi kod saya, dengan penjelasan terperinci dan pandangan matematik:
Versi 1: Pendekatan Naif (dengan Pertukaran Berlebihan)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
// Letakkan setiap nombor pada kedudukan yang betul jika ia berada dalam julat [1, n]
for (let i = 0; i < n; i++) {
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
const correctIndex = nums[i] - 1
;[nums[i], nums[correctIndex]] = [nums[correctIndex], nums[i]] // Tukar
}
}
// Cari integer positif pertama yang hilang
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
// Jika semua nombor berada pada kedudukan yang betul, nombor yang hilang ialah n + 1
return n + 1
}
Versi ini, walaupun berfungsi, mengalami pertukaran berlebihan. Bilangan pertukaran dalam senario kes terburuk mendekati , walaupun purata kes lebih hampir kepada .
Penjelasan:
Susun semula tatasusunan: Setiap nombor
nums[i]
diletakkan pada indeks "betul" (nums[i] - 1
) jika ia berada dalam julat[1, n]
dan belum berada pada kedudukan yang betul.Kenal pasti integer positif yang hilang: Selepas penyusunan semula, indeks pertama
i
di mananums[i] !== i + 1
menunjukkan bahawai + 1
ialah integer positif yang hilang.Pulangkan n + 1 jika diperlukan: Jika semua nombor dari
1
hinggan
berada pada kedudukan yang betul, nombor yang hilang ialahn + 1
.
Titik Utama:
- Penyusunan semula dalam-tempat: Kita mengubah suai tatasusunan itu sendiri untuk mengelakkan penggunaan ruang tambahan.
- Kecekapan: Kedua-dua penyusunan semula dan imbasan akhir mengambil masa , memastikan prestasi yang optimum.
Versi 2: Peningkatan Prestasi (Menghapuskan Pertukaran Berlebihan)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
let idx = 0
// Susun semula nombor kepada kedudukan yang betul jika boleh
while (idx < n) {
const correctIdx = nums[idx] - 1
if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
// Tukar tanpa menggunakan pembolehubah sementara
;[nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]
} else {
idx++
}
}
// Kenal pasti integer positif pertama yang hilang
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
return n + 1
}
Penjelasan:
Penyusunan Semula yang Cekap:
- Gelung
while
menukar elemen terus ke kedudukan yang betul (nums[idx] - 1
) hanya apabila nombor itu berada dalam julat sah[1, n]
dan belum berada pada kedudukan yang betul. - Keadaan
if
memastikan nombor yang tidak sah atau sudah betul diabaikan tanpa pertukaran yang tidak perlu.
- Gelung
Pemeriksaan Indeks Langsung:
- Semasa fasa penyusunan semula, pertukaran yang tidak sah dan operasi berlebihan dielakkan dengan memeriksa julat dan nilai
nums[idx]
.
- Semasa fasa penyusunan semula, pertukaran yang tidak sah dan operasi berlebihan dielakkan dengan memeriksa julat dan nilai
Pertukaran yang Cekap Memori:
- Penggunaan dekonstruksi (
[nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]
) menghapuskan keperluan untuk pembolehubah sementara, meminimumkan penggunaan memori.
- Penggunaan dekonstruksi (
Gelung Minimum:
- Algoritma hanya menggunakan dua lintasan linear:
- Satu untuk menyusun semula tatasusunan dalam tempat.
- Satu lagi untuk mencari integer positif pertama yang hilang.
- Algoritma hanya menggunakan dua lintasan linear:
Analisis Kerumitan:
- Kerumitan Masa:
- Gelung
while
dan gelungfor
kedua-duanya berjalan dalam , menjadikan jumlah kerumitan masa .
- Gelung
- Kerumitan Ruang:
- Algoritma beroperasi dalam tempat tanpa menggunakan sebarang struktur data tambahan, jadi kerumitan ruang ialah .
Mengapa Kod Ini Optimum:
- Prestasi: Gelung
while
yang diperkemas memastikan operasi berlebihan minimum, memaksimumkan kelajuan. - Kecekapan Memori: Penggunaan pertukaran dalam-tempat dan pengelakan pembolehubah tambahan memastikan jejak memori serendah mungkin.
Versi 3: Dioptimumkan untuk Memori (Penggunaan Ruang Minimum)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
let idx = 0
while (idx < n) {
const correctIdx = nums[idx] - 1
if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
nums[idx] = nums[correctIdx]
nums[correctIdx] = correctIdx + 1
} else {
idx++
}
}
for (let idx = 0; idx < n; idx++) {
if (nums[idx] !== idx + 1) {
return idx + 1
}
}
return nums.length + 1
}
Pengoptimuman Memori Utama:
Kemas Kini Dalam-Tempat dengan Pembolehubah Sementara Minimum:
- Bukannya menggunakan pembolehubah
temp
untuk pertukaran, kod ini terus mengubah suai tatasusunan dengannums[idx] = nums[correctIdx]; nums[correctIdx] = correctIdx + 1
. - Ini menghapuskan keperluan untuk pembolehubah sementara, mengurangkan overhead memori dengan jumlah tetap.
- Bukannya menggunakan pembolehubah
Nilai Sementara yang Kurang dalam Gelung:
- Kod ini mengira
correctIdx
terus dalam gelung, mengelakkan keperluan untuk menyimpan pembolehubah pertengahan tambahan di luar logik teras. - Penugasan pembolehubah yang lebih sedikit bermakna penggunaan memori sedikit lebih rendah bagi setiap lelaran.
- Kod ini mengira
Satu Lintasan untuk Penyusunan Semula:
- Tidak seperti kod pertama yang menggunakan keadaan bersarang dalam gelung
while
, pelaksanaan ini melengkapkan pertukaran atau memajukan indeks dengan cara yang lebih diperkemas, mengurangkan kedalaman timbunan masa jalan dan penggunaan memori. - Struktur gelung (
while
dengan satu penambahan atau penyusunan semula) lebih langsung, memerlukan nilai tambahan yang lebih sedikit.
- Tidak seperti kod pertama yang menggunakan keadaan bersarang dalam gelung
Tiada Semakan Berlebihan:
- Semakan untuk
correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]
adalah ringkas dan mencegah operasi yang tidak perlu yang boleh sementara menggunakan memori timbunan atau cache CPU. - Ini mengelakkan keperluan untuk memperuntukkan sumber tambahan untuk operasi yang tidak akan menyumbang kepada keputusan.
- Semakan untuk
Mengapa Ini Membuat Perbezaan dalam Memori:
- Penyimpanan Sementara yang Dikurangkan: Dengan tidak bergantung pada pembolehubah
temp
dan meminimumkan pengiraan pertengahan, jejak memori lebih kecil. - Pelaksanaan yang Diperkemas: Langkah yang lebih sedikit dan logik yang lebih mudah diterjemahkan kepada penggunaan memori yang lebih sedikit bagi setiap operasi.
- Penggunaan Cache yang Diperbaiki: Corak akses linear dan boleh diramal algoritma meningkatkan prestasi cache CPU, secara tidak langsung mengurangkan overhead memori.
Fokus penyelesaian ini pada kemas kini dalam-tempat langsung, penggunaan pembolehubah minimum, dan corak akses memori yang cekap menjadikannya yang terbaik dari segi kecekapan memori. Walaupun penjimatan adalah tetap (), ia cukup signifikan untuk mendaftar pada platform kompetitif seperti LeetCode, terutamanya untuk set data yang besar.
Versi 4: Karya Agung yang Dioptimumkan (Pertukaran Dalam-Tempat, Tiada Temp)
function firstMissingPositive(nums: number[]): number {
for (let i = 0; i < nums.length; i++) {
while (nums[i] !== i + 1) {
if (nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]) {
break
}
const temp = nums[i]
nums[i] = nums[temp - 1]
nums[temp - 1] = temp
}
}
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
return nums.length + 1
}
Versi akhir ini mencapai kedua-dua prestasi optimum dan penggunaan memori minimum melalui pertukaran dalam-tempat yang elegan tanpa pembolehubah sementara. Kerumitan masa kini kukuh , dan kerumitan ruang ialah , memenuhi semua keperluan. Secara matematik, kita melakukan sejenis permutasi kitaran dalam tatasusunan untuk meletakkan elemen pada kedudukan "betul" mereka.
Dengan menambah semakan (nums[i] === nums[nums[i] - 1]
), kita mencegah pertukaran yang tidak perlu. Ini secara signifikan meningkatkan prestasi, menjadikan kerumitan masa lebih hampir kepada yang dikehendaki.
Perbezaan Utama dalam Pelaksanaan:
Mengelakkan Pertukaran Berlebihan:
- Kod yang diberikan secara jelas menyemak jika
nums[i] === nums[nums[i] - 1]
sebelum melakukan pertukaran. Ini mengelakkan pertukaran yang tidak perlu apabila nombor itu sudah berada pada kedudukan yang betul atau pendua wujud. - Dalam pelaksanaan pertama, semakan ini diabaikan, berpotensi membawa kepada pertukaran berlebihan walaupun tidak perlu. Setiap pertukaran tambahan menambah overhead, terutamanya untuk tatasusunan yang lebih besar.
- Kod yang diberikan secara jelas menyemak jika
Perbandingan Indeks Langsung:
- Kod yang diberikan menggunakan
while (nums[i] !== i + 1)
untuk memastikan nombor itu berulang kali ditukar ke kedudukan yang betul sehingga ia diletakkan dengan betul atau keadaan tidak sah dipenuhi. Ini menghapuskan lelaran gelung yang tidak perlu. - Kod pertama tidak secara jelas membandingkan nombor itu dengan indeks yang dimaksudkan dalam keadaan gelung. Ia mungkin melakukan lebih banyak operasi dalam kes di mana nombor memerlukan pergerakan minimum.
- Kod yang diberikan menggunakan
Semakan Bersyarat yang Dioptimumkan:
- Dengan menggabungkan syarat dalam satu blok
if
(cth.,nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]
), kod tersebut mengelakkan overhead tambahan daripada semakan atau pengiraan yang tidak perlu.
- Dengan menggabungkan syarat dalam satu blok
Mengapa Ini Penting dalam LeetCode:
Konsistensi Masa Jalan:
- Pengukuran masa jalan LeetCode boleh sensitif terhadap operasi yang tidak perlu, seperti pertukaran berlebihan atau perbandingan tambahan.
- Kod yang diberikan meminimumkan operasi ini, yang membawa kepada masa jalan yang lebih pantas secara purata.
Kecekapan Cache:
- Pertukaran yang lebih sedikit dan logik gelung yang lebih mudah menghasilkan penggunaan cache CPU yang lebih baik. Pemproses moden mendapat manfaat daripada corak akses yang boleh diramal dan diperkemas, yang ditunjukkan oleh kod yang diberikan.
Keputusan
Berikut adalah ringkasan yang dibentangkan dalam format jadual:
Percubaan | Masa Jalan | Penggunaan Memori | Nota |
---|---|---|---|
4 (Dioptimumkan) | 1 ms | 58.8 MB | Keseimbangan terbaik prestasi dan memori. |
3 (Memori Terbaik) | 2 ms | 58.5 MB | Sedikit lebih perlahan tetapi penggunaan memori lebih rendah. |
2 (Prestasi Terbaik) | 1 ms | 58.9 MB | Masa jalan lebih pantas. |
1 (Percubaan Awal) | 3 ms | 59 MB | Paling perlahan dan penggunaan memori tertinggi. |
Jadual ini menyerlahkan pertukaran dan keoptimuman penyelesaian akhir.
Ringkasan menunjukkan bahawa percubaan untuk prestasi dan memori terbaik memang paling optimum, mencapai:
- Masa jalan 1 ms: Menyamai keputusan terpantas dari segi prestasi.
- Penggunaan memori 58.9 MB: Sedikit lebih tinggi daripada klon "memori terbaik" tetapi lebih rendah daripada percubaan lain.
Analisis:
Masa Jalan:
- Masa jalan 1 ms menunjukkan pendekatan yang dioptimumkan menghapuskan semakan berlebihan dan pertukaran yang tidak perlu.
- Kerumitan masa yang konsisten memastikan kebolehskalaan untuk set data yang lebih besar.
Memori:
- 58.9 MB adalah kompetitif, menunjukkan bahawa jejak memori adalah rendah walaupun pelaksanaan pantas. Peningkatan sedikit ini berbanding "memori terbaik" (58.5 MB) mungkin disebabkan oleh perbezaan dalam dekonstruksi atau pengoptimuman khusus enjin.
Pertukaran:
- Penyelesaian "memori terbaik" sedikit mengorbankan masa jalan untuk penggunaan memori yang lebih rendah.
- Penyelesaian "prestasi terbaik" memberi tumpuan lebih kepada kelajuan tetapi menggunakan memori yang sedikit lebih banyak.
Kesimpulan:
Penyelesaian yang dioptimumkan mencapai keseimbangan yang baik:
- Masa jalan 1 ms sepantas kod yang berprestasi terbaik.
- Penggunaan memori 58.9 MB hampir dengan keputusan memori terbaik, dengan overhead yang boleh diabaikan.
Ia adalah pilihan yang menyeluruh, terutamanya untuk senario di mana kedua-dua prestasi dan memori adalah kritikal.
Asas Matematik: Permutasi Kitaran dan Prinsip Lubang Merpati
Idea teras berputar di sekitar konsep permutasi kitaran. Kita bertujuan untuk mencipta kitaran di mana setiap nombor diletakkan pada indeks . Gelung while
berkesan melintasi kitaran ini, memastikan setiap elemen mencari tempat yang ditetapkan.
Prinsip Lubang Merpati secara halus memainkan peranan di sini. Oleh kerana kita mempunyai kedudukan yang mungkin (indeks hingga ) dan kita sedang mencari integer positif yang hilang dalam julat , jika semua nombor dari 1 hingga hadir, nombor yang hilang mestilah .
Kegilaan Berterusan...
Keghairahan saya terhadap LeetCode 41 masih berterusan. Saya sentiasa mencari pengoptimuman lanjut dan pandangan yang lebih mendalam. Sertai saya dalam usaha ini! Kongsikan pemikiran anda, penyelesaian anda sendiri, dan mari kita meneroka persilangan elegan matematik dan algoritma bersama-sama.