प्रॉम्प्ट और लागू संदर्भ
एक integer array nums और एक integer k दिए जाने पर, सॉर्ट किए गए क्रम में kth सबसे बड़ा एलिमेंट लौटाएं, न कि kth distinct मान। मान लें कि 1 <= k <= nums.length <= 100,000 और -10,000 <= nums[i] <= 10,000 है।
उदाहरण के लिए, nums = [3, 2, 1, 5, 6, 4] और k = 2 के लिए उत्तर 5 है। nums = [3, 2, 3, 1, 2, 4, 5, 5, 6] और k = 4 के लिए, उत्तर 4 है: डुप्लिकेट मान अलग-अलग रैंक पर गिने जाते हैं।
यह कोडिंग इंटरव्यू का एक प्रतिनिधि order-statistics प्रश्न है। अलग-अलग बाधाओं (constraints) के तहत पूर्ण सॉर्ट, size-k min-heap, और quickselect सभी मान्य समाधान हैं। नीचे दिया गया मुख्य समाधान randomized three-way quickselect का उपयोग करता है क्योंकि इनपुट एक इन-मेमोरी म्यूटेबल ऐरे है और केवल एक रैंक की आवश्यकता है। यह nums को म्यूटेट करता है; यदि कॉलर इनपुट को सुरक्षित रखने की मांग करता है तो पहले ऐरे को कॉपी करें।
इंटरव्यूअर क्या मूल्यांकन करता है
पहला संकेत कॉन्ट्रैक्ट की सटीकता है। "Kth सबसे बड़ा" का अर्थ घटते क्रम (descending order) में स्थिति k है, जिसमें डुप्लिकेट भी शामिल हैं। इसका अर्थ kth distinct मान, सबसे बड़े k मान, या ज़ीरो-बेस्ड ऐरे में इंडेक्स k नहीं है। बढ़ते क्रम (ascending order) में, अनुरोधित एलिमेंट का ज़ीरो-बेस्ड इंडेक्स n - k होता है।
दूसरा संकेत यह है कि क्या उम्मीदवार quickselect को सीधे रटने के बजाय विकल्पों की तुलना करके समाधान निकालता है। सॉर्टिंग O(n log n) पर सबसे सुरक्षित बेसलाइन है। एक size-k min-heap O(n log k) समय और O(k) स्पेस लेता है और स्ट्रीमिंग इनपुट के लिए भी काम करता है। Quickselect उस पार्टीशन को हटा देता है जिसमें टारगेट नहीं हो सकता है और इसका अपेक्षित समय O(n) होता है, लेकिन रैंडमाइज्ड पिवोटिंग इसके O(n^2) वर्स्ट-केस को पूरी तरह समाप्त नहीं करता है।
तीसरा संकेत एक स्पष्ट partition invariant है। केवल ऐसा कोड जो "quicksort जैसा दिखता है" पर्याप्त नहीं है। उम्मीदवार को यह बताने में सक्षम होना चाहिए कि lt से पहले, lt और i के बीच, i और gt के बीच, और gt के बाद के एलिमेंट्स के बारे में क्या ज्ञात है, और फिर यह स्पष्ट करना चाहिए कि अगले सर्च इंटरवल में अभी भी टारगेट रैंक क्यों मौजूद है।
अंत में, इंटरव्यूअर डुप्लिकेट हैंडलिंग, म्यूटेशन का प्रकटीकरण, अमान्य इनपुट का व्यवहार, रिकर्शन डेप्थ के जोखिम से बचने के लिए इटरेटिव कंट्रोल, और ऐसे टेस्ट्स की तलाश करता है जो एक साधारण ऑरेकल के साथ परिणाम की तुलना करते हैं। प्रमाण सीमा या प्रतिकूल (adversarial) परीक्षणों के बिना एक अनुकूलित एल्गोरिदम अधूरा है।
उत्तर देने से पहले स्पष्ट करने योग्य प्रश्न
- क्या kth सबसे बड़े में डुप्लिकेट गिने जाते हैं? यह उत्तर सॉर्ट की गई स्थितियों का पालन करता है, इसलिए
[5, 5, 4]के साथk = 2का उत्तर5आता है। एक distinct-rank आवश्यकता के लिए डिडप्लीकेशन या फ़्रीक्वेंसी-अवेयर चयन की आवश्यकता होगी। - क्या
kकी वैधता की गारंटी है, और क्या ऐरे खाली हो सकता है? इंटरव्यू का निर्धारित कॉन्ट्रैक्ट1 <= k <= nकी गारंटी देता है। कार्यान्वयन फिर भी उस सीमा के बाहरValueErrorफेंकता है ताकि इसका स्टैंडअलोन व्यवहार स्पष्ट रहे। - क्या फ़ंक्शन इनपुट को म्यूटेट कर सकता है? इन-प्लेस पार्टीशनिंग
O(1)ऑक्ज़िलरी स्पेस देती है। यदि म्यूटेशन वर्जित है, तो पहले कॉपी करें औरO(n)अतिरिक्त स्पेस स्वीकार करें। - क्या इनपुट पूरी तरह उपलब्ध है या स्ट्रीमिंग है? Quickselect को रैंडम एक्सेस और म्यूटेशन की आवश्यकता होती है। एक अनबाउंस्ड स्ट्रीम के लिए, इसके बजाय एक size-
kmin-heap बनाए रखें। - क्या हमें उसी डेटा पर एक क्वेरी की आवश्यकता है या कई रैंक क्वेरीज़ की? Quickselect एक रैंक के लिए आकर्षक है। एक बार सॉर्ट करना तब बेहतर हो सकता है जब बाद की कई क्वेरीज़ शुरुआती
O(n log n)कार्य को उचित ठहराती हैं। - क्या मानों की सीमा वास्तव में छोटी और स्थिर है? बताई गई सीमा में केवल 20,001 संभावित पूर्णांक मान हैं, इसलिए काउंटिंग एक मान्य विकल्प है। रेंज की चौड़ाई
Rके लिए इसकी लागतO(n + R)समय औरO(R)स्पेस है, लेकिन मान अनबाउंड होने पर इसे सामान्य समाधान के रूप में प्रस्तुत नहीं किया जाना चाहिए। - क्या वर्स्ट-केस समय सीमित होना चाहिए? Randomized quickselect अपेक्षित लीनियर समय देता है, नियतात्मक (deterministic) वर्स्ट-केस लीनियर समय नहीं। यदि सख्त वर्स्ट-केस गारंटी की आवश्यकता है, तो median-of-medians पर चर्चा करें या अनुमानित
O(n log k)समय वाले हीप को चुनें।
30-सेकंड उत्तर रूपरेखा
"Kth सबसे बड़ा एलिमेंट आरोही इंडेक्स n - k पर मौजूद आइटम है, जिसमें डुप्लिकेट शामिल हैं। सॉर्टिंग एक सरल O(n log n) बेसलाइन प्रदान करती है, और एक size-k min-heap स्ट्रीमिंग या गैर-म्यूटेटिंग इनपुट के लिए O(n log k) समय देता है। चूँकि यह समस्या एक म्यूटेबल इन-मेमोरी ऐरे में एक रैंक पूछती है, मैं इटरेटिव रैंडमाइज्ड क्विकसिलेक्ट का उपयोग करूंगा। मैं सक्रिय अंतराल को एक रैंडम पिवट से छोटे, बराबर और बड़े मानों में विभाजित करता हूँ। यदि n - k बराबर वाले बैंड में आता है, तो पिवट ही उत्तर है; अन्यथा मैं केवल उस इंडेक्स वाले हिस्से को रखता हूँ। थ्री-वे पार्टीशनिंग समान मानों को बार-बार अलग करने से बचाती है। अपेक्षित समय O(n) है, वर्स्ट-केस O(n^2) है, और ऑक्ज़िलरी स्पेस O(1) है। मैं रैंडम ऐरे के साथ-साथ सभी-समान, सॉर्ट किए गए, रिवर्स-सॉर्ट किए गए, भारी डुप्लिकेट और बाउंड्री-k मामलों पर सॉर्टिंग के विरुद्ध इसे सत्यापित करूंगा।"
चरण-दर-चरण विस्तृत उत्तर
एक ऑरेकल के साथ शुरुआत करें। आरोही क्रम में सॉर्ट करना और sorted(nums)[len(nums) - k] लौटाना समझाना आसान है और इसमें गलती की संभावना कम होती है। यह रैंक रूपांतरण स्थापित करता है और परीक्षण के लिए एक संदर्भ परिणाम प्रदान करता है। कॉपी के साथ मूल इनपुट को संरक्षित करते समय इसकी लागत O(n log n) समय और O(n) स्पेस है।
एक bounded heap कार्यक्षमता में सुधार करता है जब k छोटा होता है या डेटा धीरे-धीरे (स्ट्रीमिंग) आता है। प्रत्येक मान को min-heap में डालें और जब भी इसका आकार k से अधिक हो जाए, न्यूनतम को हटा दें। सभी मानों के बाद, रूट सबसे बड़े k एलिमेंट्स में सबसे छोटा होगा, यानी kth सबसे बड़ा। हीप k मानों को स्टोर करता है, इसलिए लागत O(n log k) समय और O(k) स्पेस है। यदि k, n के करीब है और पूरा ऐरे पहले से उपलब्ध है, तो यह लाभ कम हो जाता है।
Quickselect इस तथ्य का उपयोग करता है कि केवल एक अंतिम स्थिति मायने रखती है। घटते रैंक को target = len(nums) - k में बदलें। प्रत्येक सक्रिय अंतराल [left, right] में, एक रैंडम पिवट मान चुनें और Dutch-national-flag पार्टीशन करें। स्कैन के दौरान, निम्नलिखित बनाए रखें:
[left, lt)में पिवट से छोटे मान होते हैं।[lt, i)में पिवट के बराबर मान होते हैं।[i, gt]अवर्गीकृत (unclassified) है।(gt, right]में पिवट से बड़े मान होते हैं।
जब स्कैन समाप्त होता है, तो [lt, gt] पूरा बराबर वाला बैंड होता है। यदि target < lt है, तो छोटे-मान वाले हिस्से में जारी रखें। यदि target > gt है, तो बड़े-मान वाले हिस्से में जारी रखें। अन्यथा टारगेट बराबर वाले बैंड के अंदर आता है, इसलिए पिवट मान ही उत्तर है। यह दृष्टिकोण [7, 7, 7, 7] जैसे ऐरे के लिए महत्वपूर्ण है: एक टू-वे पार्टीशन बार-बार लगभग अपरिवर्तित काम उत्पन्न कर सकता है, जबकि थ्री-वे संस्करण एक स्कैन के बाद समाप्त हो जाता है।
import random
def find_kth_largest(nums: list[int], k: int) -> int:
if not 1 <= k <= len(nums):
raise ValueError("k must be between 1 and len(nums)")
target = len(nums) - k
left = 0
right = len(nums) - 1
while left <= right:
pivot = nums[random.randrange(left, right + 1)]
lt = left
i = left
gt = right
while i <= gt:
if nums[i] < pivot:
nums[lt], nums[i] = nums[i], nums[lt]
lt += 1
i += 1
elif nums[i] > pivot:
nums[i], nums[gt] = nums[gt], nums[i]
gt -= 1
else:
i += 1
if target < lt:
right = lt - 1
elif target > gt:
left = gt + 1
else:
return pivot
raise RuntimeError("unreachable for a valid k")i इंक्रीमेंट जानबूझकर असममित (asymmetric) है। पिवट से बड़े मान को nums[gt] के साथ स्वैप करने के बाद, i पर आने वाला नया मान अभी वर्गीकृत नहीं हुआ है, इसलिए i अपनी जगह पर रहता है। एक छोटे मान को बाईं ओर ले जाने के बाद, दोनों स्वैप की गई स्थितियों के वर्गीकरण ज्ञात होते हैं, इसलिए lt और i दोनों आगे बढ़ते हैं।
शुद्धता इनवेरिएंट और रैंक एलिमिनेशन से सिद्ध होती है। पार्टीशन प्रत्येक इनपुट एलिमेंट को सुरक्षित रखता है और बराबर बैंड से पहले सभी छोटे मानों और उसके बाद सभी बड़े मानों के साथ समाप्त होता है। इसलिए सॉर्ट किए गए क्रम में [lt, gt] के प्रत्येक इंडेक्स में पिवट मान होता है। यदि टारगेट उस बैंड के बाहर है, तो डिस्कार्ड किए गए हिस्से और बराबर बैंड में ऐसा कोई एलिमेंट नहीं है जो टारगेट इंडेक्स पर आ सके; बनाए रखा गया अंतराल अभी भी इसे समाहित किए हुए है। प्रत्येक पुनरावृत्ति या तो मान लौटाती है या अंतराल को सख्ती से छोटा करती है, इसलिए अंततः एक वैध टारगेट लौटाया जाता है।
प्रत्येक पार्टीशन वर्तमान अंतराल को एक बार स्कैन करता है। रैंडम पिवट्स के साथ, क्रमिक रूप से बनाए रखे गए अंतरालों पर अपेक्षित कुल कार्य O(n) है। लगातार प्रतिकूल (extreme) पिवट्स का क्रम n - 1, n - 2 आकार के अंतराल छोड़ सकता है, जिससे O(n^2) वर्स्ट-केस समय उत्पन्न होता है। कार्यान्वयन इटरेटिव है और इन-प्लेस पार्टीशन करता है, इसलिए इसका ऑक्ज़िलरी स्पेस O(1) है। रैंडम-नंबर-जनरेटर स्टेट और इनपुट ऐरे को ऑक्ज़िलरी स्टोरेज के रूप में नहीं गिना जाता है।
केवल निश्चित उदाहरणों के बजाय एक साधारण सॉर्ट किए गए ऑरेकल के साथ परीक्षण करें:
def oracle(nums: list[int], k: int) -> int:
return sorted(nums)[len(nums) - k]
cases = [
([3, 2, 1, 5, 6, 4], 2),
([3, 2, 3, 1, 2, 4, 5, 5, 6], 4),
([1], 1),
([7, 7, 7, 7], 3),
([-5, -1, -3, -1], 2),
(list(range(1000)), 1),
(list(range(1000)), 1000),
]
for values, rank in cases:
assert find_kth_largest(values.copy(), rank) == oracle(values, rank)कई डुप्लिकेट मानों वाले जनरेटेड ऐरे जोड़ें और ऑरेकल के साथ प्रत्येक वैध k की तुलना करें। यह भी पुष्टि करें कि k = 0, k > n, और एक खाली ऐरे प्रलेखित त्रुटि उत्पन्न करते हैं। रैंडम जनरेटर को सीड करने से विफल होने वाला प्रॉपर्टी टेस्ट पुनरुत्पादित (reproducible) हो जाता है; एकाधिक सीड्स चलाने से विभिन्न पार्टीशन पथों का परीक्षण होता है।
उच्च गुणवत्ता वाला नमूना उत्तर
"मैं डुप्लिकेट्स को अलग-अलग सॉर्टेड पोज़िशन्स मानूंगा और मानूंगा कि k वैध है। यदि ऐरे आरोही क्रम में सॉर्ट किया गया होता, तो उत्तर इंडेक्स n - k पर होता। मेरी बेसलाइन सॉर्ट और इंडेक्स करने की है, जो O(n log n) है। एक size-k min-heap O(n log k) लेता है और स्ट्रीमिंग डेटा के लिए मेरी पसंद होगी।
यहाँ हमारे पास एक क्वेरी है और हम ऐरे को म्यूटेट कर सकते हैं, इसलिए मैं randomized quickselect का उपयोग करूँगा। सक्रिय रेंज के भीतर, मैं एक रैंडम पिवट चुनता हूँ और मानों को पिवट से कम, बराबर और बड़े में विभाजित करता हूँ। थ्री-वे विभाजन महत्वपूर्ण है क्योंकि डुप्लिकेट्स को कई रैंक पर होना चाहिए और सभी-समान इनपुट को एक ही पार्टीशन में समाप्त होना चाहिए। पार्टीशन के बाद, यदि n - k बराबर रेंज के अंदर है, तो मैं पिवट लौटाता हूँ। अन्यथा मैं उस हिस्से को छोड़ देता हूँ जिसमें वह इंडेक्स नहीं हो सकता है और इसे इटरेटिव रूप से दोहराता हूँ।
इनवेरिएंट यह है कि lt से पहले सब कुछ छोटा है, lt से i तक सब कुछ बराबर है, gt के बाद सब कुछ बड़ा है, और बीच का अज्ञात भाग अभी भी अवर्गीकृत है। यह साबित करता है कि अंतिम बराबर बैंड का अपना सही सॉर्ट किया गया रैंक अंतराल है। इसलिए बनाए रखे गए हिस्से में अभी भी उत्तर मौजूद है।
अपेक्षित रनटाइम O(n) है क्योंकि एक रैंडम पिवट आमतौर पर एक बड़े हिस्से को हटा देता है, हालांकि वर्स्ट-केस O(n^2) ही रहता है। लूप और इन-प्लेस पार्टीशन O(1) ऑक्ज़िलरी स्पेस का उपयोग करते हैं। मैं यह स्पष्ट करूँगा कि फ़ंक्शन अपने इनपुट को म्यूटेट करता है, जनरेट किए गए ऐरे पर सॉर्टिंग ऑरेकल के साथ इसकी तुलना करूँगा, और इसमें डुप्लिकेट्स, सभी-समान डेटा, सॉर्ट किए गए और रिवर्स-सॉर्ट किए गए ऐरे, नेगेटिव मान, k = 1, और k = n शामिल करूँगा।"
सामान्य गलतियाँ
- Kth distinct मान लौटाना → कॉन्ट्रैक्ट में डुप्लिकेट अलग स्थितियाँ हैं → बिना डिडप्लीकेट किए सीधे आरोही इंडेक्स
n - kमें बदलें। - आरोही क्रम में इंडेक्स
kयाk - 1का उपयोग करना → दिशा रूपांतरण गलत है → जांचें किk = 1,n - 1पर औरk = n,0पर मैप होता है। - यह दावा करना कि min-heap समाधान
O(n log n)है → हीप कभी भीkएलिमेंट्स से अधिक नहीं होता →O(n log k)समय औरO(k)स्पेस बताएं। - दोनों पार्टीशन्स में रिकर्स करना → यह quicksort का काम करता है और सिंगल-रैंक लक्ष्य को नज़रअंदाज़ करता है → केवल
targetवाले अंतराल में जारी रखें। - हमेशा पहला या अंतिम पिवट चुनना → सॉर्ट किया गया या विशेष रूप से तैयार किया गया इनपुट बार-बार size-
n - 1अंतराल बना सकता है → पिवट को रैंडमाइज़ करें और वर्स्ट-केस की चेतावनी बनाए रखें। - डुप्लिकेट्स पर चर्चा किए बिना टू-वे पार्टीशन का उपयोग करना → बहुत अधिक समान मानों वाले ऐरे धीमी प्रगति कर सकते हैं → एक बराबर बैंड बनाएं और टारगेट इसके अंदर आने पर मान लौटाएं।
gtके साथ स्वैप करने के बादiको बढ़ाना → आने वाला मान अवर्गीकृत रहता है और छूट सकता है → उस मान के वर्गीकृत होने तकiको स्थिर रखें।- यह दावा करना कि रैंडमाइज़ेशन लीनियर समय की गारंटी देता है → दुर्भाग्यपूर्ण पिवट्स अभी भी संभव हैं → अपेक्षित
O(n), वर्स्ट-केसO(n^2)बताएं। - इनपुट म्यूटेशन को छिपाना → कॉलर्स मूल क्रम पर निर्भर हो सकते हैं → म्यूटेशन कॉन्ट्रैक्ट बताएं या कॉपी करें और
O(n)स्पेस का विवरण दें। - केवल दो उदाहरणों का परीक्षण करना → ऑफ़-बाय-वन, डुप्लिकेट्स और पार्टीशन बग्स अदृश्य रह जाते हैं → बाउंड्रीज़, स्ट्रक्चर्ड केसेस और जनरेटेड इनपुट्स में सॉर्टिंग के साथ तुलना करें।
फॉलो-अप प्रश्न और उन्हें कैसे संभालें
फॉलो-अप 1: यदि इनपुट एक अनबाउंडेड स्ट्रीम है तो क्या बदलता है?
Quickselect अब उपयुक्त नहीं है क्योंकि पूरा रैंडम-एक्सेस ऐरे मौजूद नहीं है। अधिकतम k मानों का एक min-heap बनाए रखें। जब तक यह k तक न पहुँच जाए तब तक पुश करें; बाद में केवल तभी रूट को बदलें जब कोई बड़ा मान आए। रूट अब तक देखा गया kth सबसे बड़ा मान है। अपडेट की लागत O(log k) है, क्वेरीज़ की लागत O(1) है, और मेमोरी O(k) है। यदि k स्वयं मनमाने ढंग से बदलता है, तो यह स्थिति अपर्याप्त हो सकती है और कॉन्ट्रैक्ट को अधिक समृद्ध ऑर्डर्ड संरचना या बनाए रखे गए डेटा की आवश्यकता होगी।
फॉलो-अप 2: क्या होगा यदि फ़ंक्शन को इनपुट को संरक्षित रखना होगा?
सबसे सरल अनुकूलन working = nums.copy() और working पर quickselect है, जिससे ऑक्ज़िलरी स्पेस O(n) में बदल जाता है। एक size-k हीप O(k) स्पेस के साथ इनपुट को संरक्षित करता है और जब k छोटा हो तो बेहतर हो सकता है। एक कॉपी को पूरी तरह से सॉर्ट करना तब सरल होता है जब n मध्यम हो या कई रैंक क्वेरीज़ सॉर्ट किए गए परिणाम का पुन: उपयोग करेंगी।
फॉलो-अप 3: क्या आप वर्स्ट-केस लीनियर समय की गारंटी दे सकते हैं?
Median-of-medians एक ऐसा पिवट चुनता है जो वर्स्ट-केस में एक स्थिर अंश (constant fraction) को छोड़ देता है, जिससे नियतात्मक O(n) चयन मिलता है। इसका कार्यान्वयन और स्थिरांक (constants) बड़े हैं, इसलिए रैंडमाइज्ड क्विकसिलेक्ट अक्सर व्यावहारिक इंटरव्यू विकल्प होता है जब तक कि आवश्यकता स्पष्ट रूप से वर्स्ट-केस सीमा की मांग न करे। एक bounded heap एक सरल अनुमानित O(n log k) विकल्प प्रदान करता है।
फॉलो-अप 4: आप छोटी पूर्णांक सीमा (small integer range) का उपयोग कैसे करेंगे?
-10,000 से 10,000 तक के मानों के लिए एक फ़्रीक्वेंसी ऐरे बनाएं, nums को स्कैन करें, फिर उच्च से निम्न की ओर फ़्रीक्वेंसी को पार करते हुए k से काउंट घटाएं। पहला बकेट जिसमें शेष रैंक शामिल है, वही उत्तर है। रेंज की चौड़ाई R = 20,001 के साथ, इसमें O(n + R) समय और O(R) स्पेस लगता है। यह नियतात्मक है और स्वाभाविक रूप से डुप्लिकेट्स को संभालता है, लेकिन रेंज बड़ी या अनबाउंड होने पर यह अनुपयुक्त हो जाता है।
फॉलो-अप 5: क्या होगा यदि इंटरव्यूअर सबसे बड़े k एलिमेंट्स को सॉर्ट किए गए क्रम में मांगता है?
एक सिंगल आर्डर स्टेटिस्टिक अब पूरा आउटपुट नहीं है। एक size-k हीप और उसके बाद हीप को सॉर्ट करने में O(n log k + k log k) समय और O(k) स्पेस लगता है। Quickselect रैंक n - k के आसपास पार्टीशन कर सकता है, जिसके बाद चयनित k मानों को सॉर्ट करने में अपेक्षित O(n + k log k) समय लगता है। म्यूटेशन, मेमोरी, वर्स्ट-केस आवश्यकताओं और आउटपुट क्रम की आवश्यकता है या नहीं, इसके आधार पर चुनें।
फॉलो-अप 6: आप रैंडमाइज्ड टेस्ट विफलताओं को प्रतिलिपि प्रस्तुत करने योग्य (reproducible) कैसे बनाते हैं?
एक इंजेक्टेड रैंडम-नंबर जनरेटर स्वीकार करें या प्रत्येक टेस्ट से पहले जनरेटर को सीड करें। विफलता पर सीड, इनपुट और k रिकॉर्ड करें। कई निश्चित सीड्स में समान इनपुट चलाएं, और सॉर्टिंग ऑरेकल के साथ प्रत्येक उत्तर की तुलना करें। यह निरंतर एकीकरण (continuous integration) में पुनरावृत्ति को बनाए रखते हुए एक एल्गोरिदम त्रुटि को किसी विशेष पिवट पथ से अलग करता है।