Leetcode上的一道算法題(Median of Two Sorted Arrays)

原題鏈接


描述:

There are two sorted arrays nums1 and nums2 of size m and n respectively.

Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)).

Example 1:

nums1 = [1, 3]
nums2 = [2]

The median is 2.0

Example 2:

nums1 = [1, 2]
nums2 = [3, 4]

The median is (2 + 3)/2 = 2.5

翻譯過來就是脖苏,給定兩個(gè)已經(jīng)升序排序過的數(shù)組,求這兩個(gè)數(shù)組的中位數(shù)定踱;中位數(shù)的定義為把兩個(gè)數(shù)組合并過后進(jìn)行升序排序后棍潘,處于數(shù)組中間的那個(gè)數(shù),此時(shí)如果合并后的數(shù)組元素個(gè)數(shù)為偶數(shù)崖媚,則為中間兩個(gè)數(shù)的平均值蜒谤。

初看起來這就是個(gè)尋找第k小數(shù)的問題,解決方案有很多至扰,最簡(jiǎn)單的就是采用歸并排序的思想把兩個(gè)數(shù)組進(jìn)行合并鳍徽,然后取中間的數(shù)就可以了。但問題在于敢课,這個(gè)題目限定了時(shí)間復(fù)雜度為O(log(m+n))阶祭,而合并算法的時(shí)間復(fù)雜度為O(nlogn),顯然不合題意直秆。另外一個(gè)方法是設(shè)置一個(gè)雙指針濒募,一開始都指向兩個(gè)數(shù)組的開頭,不停地比較兩個(gè)指針指向的元素的大小圾结,指向小元素的指針的往前移一個(gè)元素去追指向大元素的指針瑰剃,一直移動(dòng)(len1+len2)/2次后就能得到中位數(shù),但是這個(gè)算法的時(shí)間復(fù)雜度仍然不符合題意筝野,為O(n)

但是注意到這個(gè)題目給定的數(shù)組已經(jīng)是排過序的了晌姚,算法導(dǎo)論中對(duì)order statistic問題進(jìn)行過討論,因此歇竟,在有序又要求log級(jí)的時(shí)間復(fù)雜度挥唠,可以考慮分治策略,采用二分法焕议。

大方向定好了宝磨,但是并不清楚具體要怎么去完成這個(gè)二分法,我們應(yīng)該對(duì)什么去做二分?其實(shí)這個(gè)題目需要找的就是第k小的元素問題唤锉,假設(shè)我們的第k小的數(shù)是在第一個(gè)數(shù)組中找了p次世囊,然后在第二個(gè)數(shù)組中找了q次,那么滿足關(guān)系:p+q=k窿祥。進(jìn)一步的茸习,尋找第k小的數(shù)的過程就是尋找p和q的過程,k我們是知道的壁肋,但是p和q是不知道的号胚,因此事實(shí)上我們的目標(biāo)就是去搜索p(找到了p就等于找到了q),因此我們二分法的目標(biāo)浸遗,事實(shí)上就是二分k來找p猫胁。

我們先定義以下形式的函數(shù)用來尋找第k小的數(shù):

findKth(nums1, nums2, start1, len1, start2, len2, k)

nums1和nums2表示原始的兩個(gè)數(shù)組,start1跛锌、len1表示nums1數(shù)組中以start1位置開始弃秆、len1長(zhǎng)度的一個(gè)子數(shù)組;start2髓帽、lens2表示nums2數(shù)組中以start2位置開始菠赚、len2長(zhǎng)度的一個(gè)子數(shù)組;k表示從這兩個(gè)子數(shù)組中找到第k小的數(shù)郑藏。之所以提供start1衡查、len1、start2必盖、len2拌牲,是因?yàn)榻?jīng)驗(yàn)告訴我們分治法解決問題都是遞歸的,我們?cè)诙值臅r(shí)候就需要記錄這些相關(guān)的數(shù)據(jù)歌粥。

我們的外層入口就應(yīng)該是這樣子的:

if(len1+len2是偶數(shù)) {
  return (findKth(nums1, nums2, 0, len1, 0, len2, (len1+len2)/2) + 
        findKth(nums1, nums2, 0, len1, 0, len2, (len1+len2)/2 + 1)) / 2;
}
else {
  return findKth(nums1, nums2, 0, len1, 0, len2 (len1+len2)/2);
}

下面就是具體對(duì)于findKth的實(shí)現(xiàn)了塌忽。

首先,我們知道失驶,我們需要對(duì)k進(jìn)行二分:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  p = k / 2;
}

這個(gè)p怎么用呢土居?假設(shè)我們?cè)趎ums1中取nums1[start1+p-1],就表示我們?cè)趎ums1中“前進(jìn)”了p個(gè)元素嬉探,且這p個(gè)元素是有序的擦耀,相應(yīng)的,q = k - p甲馋,我們需要在nums2中前進(jìn)q個(gè)元素:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  p = k / 2;
  q = k - p;
}

假如這個(gè)時(shí)候nums1[start1 + p - 1]等于nums2[start + q - 1]埂奈,這說明kth = nums1[start1 + p - 1] = nums2[start + q - 1] = 第k小的數(shù)迄损,為什么呢定躏?這里要注意nums1和nums2都是有序的,因此它們的子數(shù)組也是有序的;假設(shè)把兩個(gè)子數(shù)組數(shù)組合并痊远,那么在nums1子數(shù)組中排在kth前面的數(shù)在合并后的數(shù)組中一定還是排在kth前面垮抗,同理在nums2子數(shù)組中排在kth前面的數(shù)在合并后的數(shù)組中也一定還是排在kth前面,它們的具體順序我們不關(guān)心也不必關(guān)心碧聪,我們只需要知道這樣一來在合并后的數(shù)組中就有p-1+q-1=k-2個(gè)數(shù)在kth前面冒版,因此kth一定就是第k小的那個(gè)數(shù)

如果nums1[start1 + p - 1]大于nums2[start + q - 1]逞姿,這里就出現(xiàn)了一個(gè)需要注意的情況辞嗡,這意味著nums2子數(shù)組中的前q個(gè)數(shù)一定都是小于nums1[start1 + p - 1]的(再次注意,nums1和nums2都是有序的)滞造,而q<k续室,這也就意味著第k小的數(shù)一定不會(huì)出現(xiàn)在nums2的子數(shù)組的前q個(gè)數(shù)中。這啟發(fā)我們?cè)谶@個(gè)時(shí)候就可以拋棄掉前q個(gè)數(shù)谒养,重新用一個(gè)新的子數(shù)組進(jìn)行搜索挺狰,注意,進(jìn)一步的搜索中由于拋棄掉了q(p)個(gè)數(shù)买窟,因此下一步在子數(shù)組中的搜索中丰泊,事實(shí)上就是在搜索第k-q(k-p)小的元素了:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  p = k / 2;
  q = k - p;
  if(nums1[start1 + p - 1] == nums2[start2 + q - 1]) {
    return nums1[start1 + p - 1];
  }
  else if(nums1[start1 + p - 1] > nums2[start2 + q - 1]) {
    return findKth(nums1, nums2, start1, len1, start2 + q, len2 - q, k - q);
  }
  else if(nums1[start1 + p - 1] < nums2[start2 + q - 1]) {
    return findKth(nums1,  nums2, start1 + p, len1 - p, start2, len2, k - p);
  }
}

上面的框架大致已經(jīng)描述清楚了我們的二分搜索算法,下一步就需要考慮退出條件始绍。

退出條件有這么一些:

  1. 在某一步搜索中子數(shù)組的長(zhǎng)度為0了瞳购,這表示有一個(gè)數(shù)組中的元素完全被拋棄掉,此時(shí)另外一個(gè)子數(shù)組的第k個(gè)元素就是我們要求的第k小的元素亏推;
  2. 在不滿足1的情況下苛败,出現(xiàn)k=1的情況,這表示需要在兩個(gè)子數(shù)組中找第1小的元素径簿,此時(shí)簡(jiǎn)單地比較一下兩個(gè)子數(shù)組的第一個(gè)元素就行了罢屈;
  3. nums1[start1 + p - 1] == nums2[start2 + q - 1]

因此可以進(jìn)一步寫成:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  if(某個(gè)子數(shù)組的長(zhǎng)度為零) {
    return 另外一個(gè)子數(shù)組的第k個(gè)元素;
  }
  
  if(k == 1) {
    return min(nums1[start1], nums2[start2]);
  }
    
  p = k / 2;
  q = k - p;
  if(nums1[start1 + p - 1] == nums2[start2 + q - 1]) {
    return nums1[start1 + p - 1];
  }
  else if(nums1[start1 + p - 1] > nums2[start2 + q - 1]) {
    return findKth(nums1, nums2, start1, len1, start2 + q, len2 - q, k - q);
  }
  else if(nums1[start1 + p - 1] < nums2[start2 + q - 1]) {
    return findKth(nums1, nums2, start1 + p, len1 - p, start2, len2, k - p);
  }
}

為了方便考慮問題,不失一般性篇亭,我們要求nums1永遠(yuǎn)是那個(gè)長(zhǎng)度較短的數(shù)組:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  if(len1 > len2) {
    return findKth(nums2, nums1, start2, len2, start2, start1);
  }
  
  if(len1 == 0) {
    return nums2[start2 + k - 1];
  }
  
  if(k == 1) {
    return min(nums1[start1], nums2[start2]);
  }
    
  p = k / 2;
  q = k - p;
  if(nums1[start1 + p - 1] == nums2[start2 + q - 1]) {
    return nums1[start1 + p - 1];
  }
  else if(nums1[start1 + p - 1] > nums2[start2 + q - 1]) {
    return findKth(nums1,  nums2, start1, len1, start2 + q, len2 - q, k - q);
  }
  else if(nums1[start1 + p - 1] < nums2[start2 + q - 1]) {
    return findKth(nums1,  nums2, start1 + p, len1 - p, start2, len2, k - p);
  }
}

此外還有一個(gè)容易被忽略的邊界問題缠捌,那就是p=k/2這一句,如果p大于len1的話译蒂,就會(huì)出現(xiàn)越界訪問的問題曼月,這個(gè)時(shí)候需要對(duì)其進(jìn)行控制:

findKth(nums1, nums2, start1, len1, start2, len2, k) {
  if(len1 > len2) {
    return findKth(nums2, nums1, start2, len2, start2, start1);
  }
  
  if(len1 == 0) {
    return nums2[start2 + k - 1];
  }
  
  if(k == 1) {
    return min(nums1[start1], nums2[start2]);
  }
    
  p = min(k / 2, len1);
  q = k - p;
  if(nums1[start1 + p - 1] == nums2[start2 + q - 1]) {
    return nums1[start1 + p - 1];
  }
  else if(nums1[start1 + p - 1] > nums2[start2 + q - 1]) {
    return findKth(nums1, start1, len1, start2 + q, len2 - q, k - q);
  }
  else if(nums1[start1 + p - 1] > nums2[start2 + q - 1]) {
    return findKth(nums1, start1 + p, len1 - p, start2, len2, k - p);
  }
}

分析到這里,基本上這個(gè)問題就解決了柔昼,不過需要說的是哑芹,對(duì)于p=min(k/2,len1)這一句,這里看起來應(yīng)該就是二分法的比較關(guān)鍵的一個(gè)地方了捕透,事實(shí)上我們把2換成3聪姿、4碴萧、5、6……都是可以的末购,因?yàn)槎址ㄋ阉魇聦?shí)上就是個(gè)碰運(yùn)氣的過程破喻,不過需要注意的是,這里p不能為0盟榴,否則在nums1中等于是沒有做“前進(jìn)”的動(dòng)作曹质,這是不允許的,因此更加健壯的描述應(yīng)該為:

p = min(max(k/2, 1), len1);

即二分過程中擎场,每一次迭代至少要在nums1中“前進(jìn)”一步羽德。

整個(gè)程序的C++代碼如下:

#include <vector>

class Solution {
private:
    double findKth(vector<int>& nums1, vector<int>& nums2, int start1, int len1, int start2, int len2, int k) {
        if (len1 > len2) {
            return findKth(nums2, nums1, start2, len2, start1, len1, k);
        }

        if (len1 == 0) {
            return nums2[start2 + k - 1];
        }

        if (k == 1) {
            return min(nums1[start1], nums2[start2]);
        }

        int p1 = min(k / 2, len1);
        int p2 = k - p1;
        if (nums1[start1 + p1 - 1] > nums2[start2 + p2 - 1]) {
            return findKth(nums1, nums2, start1, len1, start2 + p2, len2 - p2, k - p2);
        }
        else if(nums1[start1 + p1 - 1] < nums2[start2 + p2 - 1]){
            return findKth(nums1, nums2, start1 + p1, len1 - p1, start2, len2, k - p1);
        }
        else {
            return nums1[start1 + p1 - 1];
        }

    }

public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        int len = nums1.size() + nums2.size();

        if (!(len & 0x01)) {
            return (findKth(nums1, nums2, 0, nums1.size(), 0, nums2.size(), len / 2)
                + findKth(nums1, nums2, 0, nums1.size(), 0, nums2.size(), len / 2 + 1)
                ) / 2.0f;
        }
        else {
            return findKth(nums1, nums2, 0, nums1.size(), 0, nums2.size(), len / 2 + 1);
        }
    }
};

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市迅办,隨后出現(xiàn)的幾起案子玩般,更是在濱河造成了極大的恐慌,老刑警劉巖礼饱,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坏为,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡镊绪,警方通過查閱死者的電腦和手機(jī)匀伏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝴韭,“玉大人够颠,你說我怎么就攤上這事¢” “怎么了履磨?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)庆尘。 經(jīng)常有香客問我剃诅,道長(zhǎng),這世上最難降的妖魔是什么驶忌? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任矛辕,我火速辦了婚禮,結(jié)果婚禮上付魔,老公的妹妹穿的比我還像新娘聊品。我一直安慰自己,他們只是感情好几苍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布翻屈。 她就那樣靜靜地躺著,像睡著了一般妻坝。 火紅的嫁衣襯著肌膚如雪伸眶。 梳的紋絲不亂的頭發(fā)上惊窖,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音赚抡,去河邊找鬼爬坑。 笑死纠屋,一個(gè)胖子當(dāng)著我的面吹牛涂臣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播售担,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼赁遗,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了族铆?” 一聲冷哼從身側(cè)響起岩四,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哥攘,沒想到半個(gè)月后剖煌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡逝淹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年耕姊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栅葡。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茉兰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出欣簇,到底是詐尸還是另有隱情规脸,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布熊咽,位于F島的核電站莫鸭,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏横殴。R本人自食惡果不足惜黔龟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望滥玷。 院中可真熱鬧氏身,春花似錦、人聲如沸惑畴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽如贷。三九已至陷虎,卻和暖如春到踏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尚猿。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工窝稿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凿掂。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓伴榔,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親庄萎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子踪少,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容