幾道常見算法面試題

本文由玉剛說寫作平臺提供寫作贊助,版權(quán)歸玉剛說所有。
原作者:Jiantao
版權(quán)聲明:未經(jīng)玉剛說許可,不得以任何形式轉(zhuǎn)載。

數(shù)據(jù)結(jié)構(gòu)和算法有多重要?

我想有追求的程序員都不會放過它的。 打個比方,在金庸的武俠世界里,數(shù)據(jù)結(jié)構(gòu)和算法它就像一門上乘的內(nèi)功心法,一旦掌握了它,各種武功信手拈來,毫無壓力(張無忌就是一個典型的例子);對于程序員來說,它能決定你在技術(shù)這條道路上能走多遠。

本文主要涉及數(shù)組、字符串、List這幾個數(shù)據(jù)結(jié)構(gòu),然后通過解答和分析幾道常見的面試題,從中分享一些我的學(xué)習(xí)心得和解題套路,希望對你有幫助。

題目1:翻轉(zhuǎn)句子

題目: 給定一個英文句子,每個單詞之間都是由一個或多個空格隔開,請翻轉(zhuǎn)句子中的單詞順序(包括空格的順序),但單詞內(nèi)字符的順序保持不變。例如輸入"www google com ",則應(yīng)輸出" com google www"。

如果你經(jīng)常關(guān)注算法相關(guān)文章,這道題應(yīng)該會比較熟悉,各種博客和書籍上都有出現(xiàn),不熟悉也沒關(guān)系,現(xiàn)在我們就一起來嘗試解答下。這里要注意題意和網(wǎng)上流傳的題目有個不同點:網(wǎng)上基本都是單詞間有且只有一個空格。而此題需要考慮一個或多個空格的情況

解題思路

試想一下,如果將整個字符串翻轉(zhuǎn),結(jié)果是句子是反轉(zhuǎn)了,但單詞內(nèi)的字符順序也翻轉(zhuǎn)了。如果要保證單詞內(nèi)順序不變,只需要再將每個單詞翻轉(zhuǎn)一下就滿足要求了。

由于題中“www google com ”字符串較長,我就以" hello world"為例分析下這個過程,請看下圖。

yugang-v1-0-solution.png

圖 1.0 翻轉(zhuǎn)句子,但保證句子中單詞內(nèi)部字符順序。

注:(1)字符串" hello world"初始狀態(tài),注意首字符是空格。 (2)將" hello world"整個句子翻轉(zhuǎn)后的樣子??梢钥闯霾粌H翻轉(zhuǎn)了句子中單詞的順序(包括空格),連單詞內(nèi)的字符順序也翻轉(zhuǎn)了。(3) 定義兩個指針p1、p2都指向句子的首字符。 (4)首字符d,不是空格,此時p1指針不動,p2指針向右移動1位,指向字符 l。(移動p2指針目的:檢查單詞的結(jié)束位置。) (5)由于第二個字符為 l ,也不是空格,p2繼續(xù)向右移動1位。(6)多次移動后,p2指針在第一個空格處停下來,此時就能得知p2-1為該單詞的結(jié)束位置。(7)反轉(zhuǎn)兩個指針(p1、p2-1)中間的字符串。(8)交換后,重置兩個指針位置p1=p2++。以此類推,繼續(xù)尋找下一個單詞并翻轉(zhuǎn),直到指針移動到句子末尾就結(jié)束循環(huán)。

此思路的關(guān)鍵是:1. 實現(xiàn)一個函數(shù)/方法,翻轉(zhuǎn)字符串中的一段。 2. 判斷并獲取句子中的單詞,注意空格。

測試用例

  • 功能測試:多個單詞、1個單詞、單詞間只有一個空格、單詞間有多個空格。
  • 特殊輸入測試:空字符、字符串中只有空格、null對象(指針)。

編碼實現(xiàn)

  • Java代碼
/**
 * @param chars 原字符串
 * @param start 大于等于0
 * @param end   小于 length
 * @return
 */
private char[] v1_0_reverse(char[] chars, int start, int end) {

    // str 判斷null, 索引有效值判斷
    if (chars == null || start < 0 || end >= chars.length || start >= end) {
        return chars;
    }

    while (start < end) {
        // 收尾字符互換,直到替換完成。
        char temp = chars[start];
        chars[start] = chars[end];
        chars[end] = temp;
        start++;
        end--;
    }
    return chars;
}

private String v1_0_solution(String sentence) {
    if (sentence == null || sentence.isEmpty()) {
        return sentence;
    }

    int length = sentence.length();
    // 第一步翻轉(zhuǎn)所有字符
    char[] chars = v1_0_reverse(sentence.toCharArray(), 0, length - 1);
    System.out.println(new String(chars));

    // 第二步翻轉(zhuǎn)每個單詞(重點:怎么找到單詞)
    int start = 0, end = 0;
    while (start < length) {
        if (chars[start] == ' ') {
            // 遇到空格就向右邊繼續(xù)查找
            start++;
            end++;
        } else if (end == length || chars[end] == ' ') {
            // 遇到空格或者已經(jīng)到了字符串末尾,此時翻轉(zhuǎn)找到的單詞內(nèi)部字符,這里需要注意end-1
            chars = v1_0_reverse(chars, start, end - 1);
            System.out.println(new String(chars));
            // 重新制定檢查索引start
            start = end++;
        } else {
            // end加1,為了檢查單詞是否結(jié)束
            end++;
        }
    }
    return new String(chars);
}
  • C++ 代碼實現(xiàn)
// 反轉(zhuǎn)字符串
void Reverse(char *pBegin, char *pEnd)
{
    if(pBegin == NULL || pEnd == NULL)
        return;

    while(pBegin < pEnd)
    {
        char temp = *pBegin;
        *pBegin = *pEnd;
        *pEnd = temp;

        pBegin ++, pEnd --;
    }
}

// 翻轉(zhuǎn)句子中單詞順序,但保證單詞內(nèi)字符順序不變。
char* ReverseSentence(char *pData)
{
    if(pData == NULL)
        return NULL;

    char *pBegin = pData;

    char *pEnd = pData;
    while(*pEnd != '\0')
        pEnd ++;
    pEnd--;

    // 翻轉(zhuǎn)整個句子
    Reverse(pBegin, pEnd);

    // 翻轉(zhuǎn)句子中的每個單詞
    pBegin = pEnd = pData;
    while(*pBegin != '\0')
    {
        if(*pBegin == ' ')
        {
            pBegin ++;
            pEnd ++;
        }
        else if(*pEnd == ' ' || *pEnd == '\0')
        {
            Reverse(pBegin, --pEnd);
            pBegin = ++pEnd;
        }
        else
        {
            pEnd ++;
        }
    }
    return pData;
}

如果你在面試的時候遇到這道題,并且很容易就想到了這個算法,有經(jīng)驗的面試官就會在這道題基礎(chǔ)上加點難度,繼續(xù)考查面試者。so,第二道題來了:

題目:接上題,面試官繼續(xù)提問,我們得到的" com google www"需要被用作一個URL的參數(shù),所以這里需要的處理是去掉開頭結(jié)尾的無效空格,并將兩個單詞中間的每一個空格都替換為"%20"。例如" com google www"應(yīng)被轉(zhuǎn)換為"com%20%20google%20www",請給出轉(zhuǎn)換函數(shù)。

解題思路

  • 第一步去掉收尾的無效空格;比如" com google www"去掉后得到"com google www"。
  • 第二步將兩個單詞中間的每一個空格都替換為"%20"。

還是以" hello world"為例,簡單分析下解題過程,請看下圖。

反轉(zhuǎn)字符串02.png

圖 1.1 剔除收尾無效空格,并將單詞間的每一個空格都替換為"%20"。

注:(1)字符串" hello world",這里注意首字符是空格。 (2)剔除首尾空格后。 (3)對原字符串進行擴容。newLen = len + 2 x blackCount;這里解釋下新數(shù)組的長度是如何計算的,由于是將每一個空格都替換為"%20",就相當(dāng)于原來占一個字符替換后要占三個字符,換言之,每一個空格就會多出兩個字符長度,所以就有前面的表達式。 (4) 定義兩個指針p1、p2,分別指向len-1和newLen-1位置。 (5)判斷p1指針是否指向空格,如果是則在p2處開始插入字符“%20”,不是則將p1指向的值復(fù)制給p2并將兩個指針往左移動一位。這里將p1指向的字符 d 賦值給p2,并將兩個指針向左移動一位。 (6)將p1指向的字符 l 賦值給p2,并移動指針。 (7)多次賦值和移動后,p1指向了第一個空格。 (8)在p2處依次插入字符 0 、 2% ,并指針p2向左移動三位,結(jié)束后將p1向左移動一位,此時p1、p2重合結(jié)束循環(huán)。

測試用例

  • 功能測試:前后有無空格情況、中間一個或多個空格情況。
  • 特殊輸入測試:空字符、字符串中只有空格、null對象(指針)。

編碼實現(xiàn)

  • Java代碼
private String v1_1_solution(String sentence) {
    if (sentence == null || sentence.isEmpty()) {
        return sentence;
    }

    // 去掉字符串收尾的空格
    sentence = trim(sentence);
    int len = sentence.length();
    char[] chars = sentence.toCharArray();
    int count = getSpaceCount(sentence);
    int newLen = 2 * count + len;
    // 擴容,內(nèi)部使用System.arraycopy 方法實現(xiàn)。
    chars = Arrays.copyOf(chars, newLen);

    int index = len - 1;
    int newIndex = newLen - 1;
    while (index >= 0 && newIndex > index) {
        if (chars[index] == ' ') {
            chars[newIndex--] = '0';
            chars[newIndex--] = '2';
            chars[newIndex--] = '%';
        } else {
            chars[newIndex--] = chars[index];
        }
        index--;
    }

    return new String(chars);
}

/**
 * 剔除字符串收尾的空格
 *
 * @param origin
 * @return
 */
private String trim(String origin) {
    char[] chars = origin.toCharArray();
    int length = chars.length;
    int st = 0;
    while (st < length && chars[st] == ' ') {
        st++;
    }

    while (st < length && chars[length - 1] == ' ') {
        length--;
    }

    // 如果收尾有空格,就截取生成新的字符串
    if (st > 0 || length < chars.length) {
        origin = new String(chars, st, (length - st));
    }
    return origin;
}

private int getSpaceCount(String sentence) {
    char[] chars = sentence.toCharArray();
    int count = 0;
    for (char c : chars) {
        if (c == ' ') {
            count++;
        }
    }
    return count;
}

  • C++實現(xiàn)
/* 去掉收尾空格:將原字符串截取后返回新字符串 */
void trim(char *strIn, char *strOut){
    int i = 0;
    int j = strlen(strIn) - 1;

    while(strIn[i] == ' ')
        ++i;

    while(strIn[j] == ' ')
        --j;
    strncpy(strOut, strIn + i , j - i + 1);
    strOut[j - i + 1] = '\0';
}

/*length 為字符數(shù)組string的總?cè)萘?/
void replaceBlank(char string[], int length)
{
    if(string == NULL && length <= 0)
        return;

    /*originalLength 為字符串string的實際長度*/
    int originalLength = 0;
    int numberOfBlank = 0;
    int i = 0;
    while(string[i] != '\0')
    {
        ++ originalLength;

        if(string[i] == ' ')
            ++ numberOfBlank;

        ++ i;
    }

    /*newLength 為把空格替換成'%20'之后的長度*/
    int newLength = originalLength + numberOfBlank * 2;
    if(newLength > length)
        return;

    int indexOfOriginal = originalLength;
    int indexOfNew = newLength;
    while(indexOfOriginal >= 0 && indexOfNew > indexOfOriginal)
    {
        if(string[indexOfOriginal] == ' ')
        {
            string[indexOfNew --] = '0';
            string[indexOfNew --] = '2';
            string[indexOfNew --] = '%';
        }
        else
        {
            string[indexOfNew --] = string[indexOfOriginal];
        }

        -- indexOfOriginal;
    }
}

題目2:調(diào)整數(shù)組中元素順序

題目: 給定一個整數(shù)數(shù)組,請實現(xiàn)一個函數(shù)來調(diào)整數(shù)組中數(shù)字的順序,使得所有奇數(shù)都位于偶數(shù)之前。

解題思路

此題比較簡單,我最先想到的解法是這樣:我們維護兩個指針(索引),一個指針指向數(shù)組的第一個數(shù)字,稱之為頭指針,向右移動;一個指針指向最后一個數(shù)字,稱之為尾指針,向左移動。

yugang-dsaa-v2.0.png

圖2.0 調(diào)整數(shù)組{2,1,3,6,4,7,8,5}使得奇數(shù)位于偶數(shù)前面的過程。

注:(1)初始化兩個指針P1、P2,分別指向數(shù)組的頭部和尾部。(2)由上一步得知,指針P1指向的數(shù)字是偶數(shù)2,而P2指向的數(shù)字是奇數(shù)5,滿足條件,我們交換這兩個數(shù)字。(3) P1繼續(xù)向右移動直到指向偶數(shù)6,P2繼續(xù)向左移動直到指向奇數(shù)7。(4)交換兩個指針指向的數(shù)字。(5)P1,P2繼續(xù)移動后重疊,表明所有奇數(shù)已位于偶數(shù)前面了。

循環(huán)結(jié)束條件:兩個指針重疊時或P2指針移動到了P1指針的前面,此時退出循環(huán)。
可以看出此算法,一次循環(huán)搞定,所以時間復(fù)雜度O(n), 由于在原數(shù)組上操作,所以空間復(fù)雜度O(1)。

測試用例

  • 功能測試:全是奇數(shù)、全是偶數(shù)、奇偶數(shù)存在但已排好序/未排好序。
  • 特殊輸入測試: null對象、數(shù)組元素為0、有負數(shù)情況。

編碼

  • Java實現(xiàn)
private int[] v2_0_solution(int[] nums) {
     if (nums == null || nums.length <= 1) {
         return nums;
     }
     int st = 0;
     int end = nums.length - 1;

     while (st < end) {
         // find even number
         if (isOdd(nums[st])) {
             st++;// 奇數(shù),索引右移
         } else if (!isOdd(nums[end])) {
             end--;// 偶數(shù),索引左移
         } else {
             // 奇偶數(shù)互換
             int temp = nums[st];
             nums[st] = nums[end];
             nums[end] = temp;
             st++;
             end--;
         }
     }
     return nums;
 }

 // 與1做按位運算,不為0就是奇數(shù),反之為偶數(shù)
 private boolean isOdd(int n) {
     return (n & 1) != 0;
 }

  • C++實現(xiàn)
// 互換
void swap(int* num1, int* num2)
{
    int temp = *num1;
    *num1 = *num2;
    *num2 = temp;
}

//判斷是否為奇數(shù)
bool isOdd(int data)
{
    return (data & 1) != 0;
}

//奇偶互換
void oddEvenSort(int *pData, unsigned int length)
{
    if (pData == NULL || length == 0)
        return;

    int *pBegin = pData;
    int *pEnd = pData + length - 1;

    while (pBegin < pEnd)
    {
        //如果pBegin指針指向的是奇數(shù),正常,向右移
        if (isOdd(*pBegin))  
        {
            pBegin++;
        }
        //如果pEnd指針指向的是偶數(shù),正常,向左移
        else if (!isOdd(*pEnd))
        {
            pEnd--;
        }
        else
        {
            //否則都不正常,交換
            swap(pBegin, pEnd);
        }
    }
}

有經(jīng)驗的面試官又來了,題目難度需要升下級,??~

題目: 接上題,面試官會繼續(xù)要求改造此函數(shù)使其能夠保證原先輸入數(shù)組的奇數(shù)內(nèi)部順序以及偶數(shù)內(nèi)部順序,即如果輸入為{2,1,3,6,4,7,8,5},則輸出應(yīng)為{1,3,7,5,2,6,4,8},奇數(shù)之間的相互順序和偶數(shù)之間的相互順序不得被改變。

解題思路

要想保證原數(shù)組內(nèi)元素的順序,可使用O(n)的temp數(shù)組空間按順序緩存偶數(shù),奇數(shù)依次放到原數(shù)組前面,最后將temp中偶數(shù)取出放在原數(shù)組后面。

yugang-dsaa-v2-1.png

圖 2.1 借助O(n)的temp數(shù)組緩存偶數(shù),進而保證原數(shù)組順序。

注: 變量解釋:st為即將插入的奇數(shù)在原數(shù)組中的索引,evenCount為緩存的偶數(shù)個數(shù)。(1)初始化和原數(shù)組相同長度的數(shù)組temp,指針p1指向首個元素,st=eventCount=0。 (2)將p1指向的偶數(shù) 2 放入在temp中,evenCount自加1。 (3)由于p1指針向右移動一位指向的是奇數(shù) 1 ,所以將p1指向的值賦值給Array[st],此時還st=0,賦值完成后st自加1。 (8)依次邏輯,直到循環(huán)結(jié)束時,已完成原數(shù)組中奇數(shù)元素按順序插入到了頭部,偶數(shù)按順序緩存在了temp數(shù)組中,即圖中狀態(tài)。

上圖展示了偶數(shù)按順序緩存到temp數(shù)組中,奇數(shù)按順序放到原數(shù)組前面。最后將temp數(shù)組中的偶數(shù)依次按序放在原數(shù)組后面,這個過程較簡單,就沒體現(xiàn)到圖中,具體請看下面代碼實現(xiàn)。

測試用例

同上一題。這里就省略了。

編碼

  • Java實現(xiàn)
private int[] v2_1_solution(int[] nums) {
     if (nums == null || nums.length <= 1) {
         return nums;
     }

     int st = 0;
     int evenCount = 0;
     int[] temp = new int[nums.length];
     for (int i = 0; i < nums.length; i++) {
         if (!isOdd(nums[i])) {
             evenCount += 1;
             temp[evenCount - 1] = nums[i];
         } else {
             if (st < i) {
                 // 將奇數(shù)依次放在原數(shù)組前面
                 nums[st] = nums[i];
             }
             st++;
         }
     }

     if (evenCount > 0) {
         for (int i = st; i < nums.length; i++) {
             nums[i] = temp[i - st];
         }
     }
     return nums;
}
  • C++實現(xiàn)
void v2_1_solution(int* nums,unsigned int len)
{
     if (!nums || len <= 1) {
         return;
     }
     int st = 0;
     int evenCount = 0;
     // 申請的內(nèi)存空間temp
     int temp[len];
     for (int i = 0; i < len; i++) {
         if (!isOdd(nums[i])) {
             evenCount += 1;
             temp[evenCount - 1] = nums[i];
         } else {
             if (st < i) {
                 // 將奇數(shù)依次放在原數(shù)組前面
                 nums[st] = nums[i];
             }
            st++;
         }
     }
     // 將temp中偶數(shù)取出放在原數(shù)組后面
     if (evenCount > 0) {
         for (int i = st; i < len; i++) {
             nums[i] = temp[i - st];
         }
     }
 }

題目3:利用數(shù)組實現(xiàn)一個簡易版的List

題目:請利用數(shù)組實現(xiàn)一個簡易版的List,需要實現(xiàn)poll和push兩個接口,前者為移除并獲得隊頭元素,后者為向隊尾添加一個元素,并要求能夠自動擴容。

解題思路

還是以“hello world”為例,作圖分析下。

用數(shù)組實現(xiàn)List.png

圖 3.0 List的push和poll過程

注:(1) 初始化List,數(shù)組默認容量len為8,size=0。(容量小一點方便作圖,實際容量看需求而定。) (2) 隊尾添加字符 h ,size++。 (3)添加len-1個字符后,size指向數(shù)組最后一個位置。 (4)如果再添加字符 O ,由于size++滿足條件:大于等于len,此時需要先對List先擴容,擴容后,再進行添加字符操作。 (5) 接著繼續(xù)添加,直到“hello world”都push到List中。 (6)這是一個poll過程,可以看出即獲取了對頭元素 h ,并且整個數(shù)組中元素向左移動一位來實現(xiàn)移除效果。

關(guān)于擴容:每次擴容多少?上圖例子是變?yōu)樵瓉淼?倍。像ArrayList則是這樣 int newCapacity = oldCapacity + (oldCapacity >> 1),可以看出擴容后大小 = 原來大小 + 原來大小/2。所以擴容多少由你自己決定。

此題關(guān)鍵是在怎么實現(xiàn)poll和push兩個接口上。

  • push(添加元素):按索引添加到數(shù)組中,size大于等于數(shù)組長度時就先擴容。
  • poll(獲取并移動對頭元素):移動數(shù)組并置空最后一個元素。

測試用例

  • 功能測試: 添加、移除元素
  • 特殊測試: 添加大量數(shù)據(jù)(測試擴容)、移除所有元素、null數(shù)據(jù)

編碼

  • Java實現(xiàn)
private static final int DEFAULT_CAPACITY = 16;
private Object[] elementData;
// 實際存儲的元素數(shù)量
//  The size of the List (the number of elements it contains).
private int size;

public CustomList() {
    elementData = new Object[DEFAULT_CAPACITY];
}

public CustomList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = new Object[DEFAULT_CAPACITY];
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " +
                initialCapacity);
    }
}

/**
 * 移除并獲得隊頭元素
 *
 * @return
 */
public Object poll() {
    if (size <= 0){
        throw new IndexOutOfBoundsException(" list is empty .");
        }
    // 獲取隊頭第一個元素
    Object oldValue = elementData[0];

    // 數(shù)組元素左移一位 & 最后一位元素置空
    System.arraycopy(elementData, 1, elementData, 0, size - 1);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

/**
 * 向隊尾添加一個元素
 *
 * @param item
 */
public void push(Object item) {
    ensureExplicitCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = item;
}

@Override
public String toString() {
    return Arrays.toString(elementData);
}

// 這里擴容參考的ArrayList,具體實現(xiàn)請點擊文末源代碼鏈接前往查看。
private void ensureExplicitCapacity(int minCapacity) {
    // 期望的最小容量大于等于現(xiàn)有數(shù)組的長度,則進行擴容
    if (minCapacity - elementData.length >= 0)
        grow(minCapacity);
}

  • C++實現(xiàn)
class List { 
  private: 
    int expansionSize = 16;//每次擴容個數(shù)
    int elemsSize = 0;//數(shù)組長度
    int dataIndex = -1;//最后一位元素下標(biāo)
    T* elems;          //元素 
 
  public: 
    List(){
        elemsSize = 0;
        dataIndex = -1;
    }
    List(int initialCapacity){
        if (initialCapacity<=0) { 
            throw out_of_range("initialCapacity must > 0"); 
        }
        elemsSize = initialCapacity;
        elems = new T[initialCapacity];
    }
    void push(T const&);  // 入棧
    T poll();             // 出棧
    int size();
    ~List(){
        if(elemsSize>0){
            delete []elems;
        }
    }
}; 
 
template <class T>
void List<T>::push (T const& elem) 
{ 
    if(elemsSize <= 0){//初始化數(shù)組
        elemsSize = expansionSize;
        elems = new T[elemsSize];
    }
    if(dataIndex+1 >= elemsSize){//數(shù)組擴容
        elemsSize += expansionSize;
        T* newElems = new T[elemsSize];
        for(int i=0;i<=dataIndex;i++){
            newElems[i] = elems[i];
        }
        delete[]elems;
        elems = newElems;
    }
    dataIndex++;
    elems[dataIndex] = elem;
} 
 
template <class T>
T List<T>::poll () 
{ 
    if (dataIndex<0) { 
        throw out_of_range("List<>::poll(): empty List"); 
    }
    T poll = elems[0]; //獲取第一位
    for(int i=1;i<=dataIndex;i++){//后面元素向左移
        elems[i-1] = elems[i];
    }
    dataIndex--;
    return poll;
} 

template <class T>
int List<T>::size () 
{ 
    return dataIndex+1;
}

題目4:數(shù)組中出現(xiàn)次數(shù)超過一半的數(shù)

題目: 一個整數(shù)數(shù)組中有一個數(shù)字出現(xiàn)的次數(shù)超過了數(shù)組長度的一半,請找出這個數(shù)字。如輸入一個長度為9的數(shù)組{1,2,3,2,2,2,5,4,2},由于2出現(xiàn)了5次,超過了數(shù)組長度的一半,因此應(yīng)輸出2。

解題思路

如果我們將數(shù)組排序,那么排序后位于數(shù)組中間的的數(shù)字一定是那個出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字。這個數(shù)就是統(tǒng)計學(xué)上的中位數(shù)。

此題關(guān)鍵在于快速排序算法,我們一起看看下面這張圖,來理解下快排的思想。

Sorting_quicksort_anim.gif

圖 4.0 快速排序過程動圖,圖片來源Wikipedia。

快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。

  • 步驟為:

    • 從數(shù)列中挑出一個元素,稱為"基準(zhǔn)"(pivot),
    • 重新排序數(shù)列,所有比基準(zhǔn)值小的元素擺放在基準(zhǔn)前面,所有比基準(zhǔn)值大的元素擺在基準(zhǔn)后面(相同的數(shù)可以到任何一邊)。在這個分區(qū)結(jié)束之后,該基準(zhǔn)就處于數(shù)列的中間位置。這個稱為分區(qū)(partition)操作。
    • 遞歸地(recursively)把小于基準(zhǔn)值元素的子數(shù)列和大于基準(zhǔn)值元素的子數(shù)列排序。

遞歸到最底部時,數(shù)列的大小是零或一,也就是已經(jīng)排序好了。這個算法一定會結(jié)束,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最后的位置去。

測試用例

  • 存在(或者不存在)次數(shù)超過數(shù)組長度一半的數(shù)。
  • 特殊用例: null、空元素、 只有一個數(shù)。

編碼

  • Java實現(xiàn)
private int v4_0_solution(int[] array) {
     if (array == null || array.length < 1) {
         throw new IllegalArgumentException(" array is empty. ");
     }

     int head = 0;
     int tail = array.length - 1;
     // 快速排序
     qSort(array, head, tail);
     int middle = array.length >> 1;
     int result = array[middle];
     // 判斷中位數(shù)是否為超過數(shù)組長度一半的數(shù)。
     if (checkMoreThanHalf(array, result)) {
         return result;
     } else {
         throw new IllegalArgumentException("not find the number.");
     }
}

public void qSort(int[] arr, int head, int tail) {
    // 參數(shù)合法性及結(jié)束條件
    if (head >= tail || arr == null || arr.length <= 1) {
        return;
    }
    // 取中間數(shù)為基準(zhǔn)值
    int i = head, j = tail, pivot = arr[(head + tail) / 2];
    while (i <= j) {
        // 處理大于等于基準(zhǔn)數(shù)情況
        while (arr[i] < pivot) {
            ++i;
        }
        while (arr[j] > pivot) {
            --j;
        }
        // 直接互換,沒有基準(zhǔn)數(shù)歸位操作
        if (i < j) {
            swap(arr, i, j);
            ++i;
            --j;
        } else if (i == j) {
            ++i;
        }
    }
    // 遞歸處理基準(zhǔn)數(shù)分隔的兩個子數(shù)列。
    qSort(arr, head, j);
    qSort(arr, i, tail);
} 
 
private boolean checkMoreThanHalf(int[] nums, int number) {
     int times = 0;
     for (int i = 0; i < nums.length; i++) {
         if (nums[i] == number) {
             times++;
         }
     }
     return times * 2 > nums.length;
}
  • C++ 實現(xiàn)
// 快速排序:遞歸方式 參考Wikipedia
void quick_sort_recursive(int arr[], int start, int end) {
    if (start >= end)
        return;
    int mid = arr[end];
    int left = start, right = end - 1;
    while (left < right) {
        while (arr[left] < mid && left < right)
            left++;
        while (arr[right] >= mid && left < right)
            right--;
        std::swap(arr[left], arr[right]);
    }
    if (arr[left] >= arr[end])
        std::swap(arr[left], arr[end]);
    else
        left++;
    quick_sort_recursive(arr, start, left - 1);
    quick_sort_recursive(arr, left + 1, end);
}

int main()
{
    //存在出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字
    //int data[] = {1, 2, 3, 2, 2, 2, 5, 4, 2};
    //不存在出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字
    //int data[] = {4, 5, 1, 6, 2, 7, 3, 8};
    // 出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)字都出現(xiàn)在數(shù)組的前/后半部分
    int data[] = {2, 2, 2, 2, 2, 1, 3, 4, 5};
    //int data[] = {1, 3, 4, 5, 2, 2, 2, 2, 2};
    int len = sizeof(data)/sizeof(int);
    printf("length =  %d \n", len);
    quick_sort_recursive(data, 0, len -1);
    for(int i=0;i<len;i++){
       printf(" %d ", data[i]);
    }
    printf("\n");
    int middle = len >> 1;
    int result = data[middle];
    if(CheckMoreThanHalf(data, len, result)){
       printf("the number is  %d ", result);
    }else{
        printf("not find the number.");
    }
    return 0;
}

有經(jīng)驗的面試官又來了,題目難度需要升下級,??~

題目:這個題目有很多變種,其中一個引申為輸入的是一個對象數(shù)組,該對象無法比較大小,只能利用equals()方法比較是否相等,此時該如何解(若要用到O(n)的輔助空間,能否避免?)。

解題思路

數(shù)組中有一個元素出現(xiàn)的次數(shù)超過數(shù)組長度的一半,也就是說它出現(xiàn)的次數(shù)比其他所有元素出現(xiàn)次數(shù)的和還要多。

因此我們可以考慮在遍歷數(shù)組的時候保存兩個值: 一個是數(shù)組中的一個元素, 一個是次數(shù)。當(dāng)我們遍歷到下一個元素的時候,如果下一個元素和我們之前保存的元素相等(equals返回true),則次數(shù)加1;如果下一個元素和我們之前保存的不相等,則次數(shù)減1。如果次數(shù)為0,我們需要保存下一個元素,并把次數(shù)設(shè)為1。由于我們要找的數(shù)字出現(xiàn)的次數(shù)比其他所有數(shù)字出現(xiàn)的次數(shù)之和還要多,那么要找的數(shù)字肯定是最后一次把次數(shù)設(shè)為1時對應(yīng)的那個元素。

怎么樣簡單吧,還是畫張圖來理解一下。

數(shù)組中出現(xiàn)次數(shù)超過一半的數(shù).png

圖4.0 數(shù)組中出現(xiàn)次數(shù)超過數(shù)組長度一半的數(shù)。

注:雖然途中數(shù)組元素類型是整型,但其思想適用于任何類型。(1) 數(shù)組初始狀態(tài),times只是一個標(biāo)記變量,默認為0, result為最后一次設(shè)置times=1時的那個元素,默認為NULL。(2)開始循環(huán),i=0時,times設(shè)置為1,并將第一個元素 1 賦值給result變量。 (3)i=1時,由于此時Array[i]的值為 2 ,不等于result,所以times--,操作后times等于0,result不變。(4)i=2時,由于此時times==0,所以重新設(shè)置times=1,result= Array[2]= 3 。(5)i=3時,和(3)類似,由于此時Array[i]的為2,不等于result,所以times--,操作后times等于0,result不變還是等于3。(6)依次邏輯,一直遍歷到末尾,即i=8時,邏輯同上,可以求出times=1,result=2;ok,循環(huán)結(jié)束。

到這里得出result=2,那這個2是不是我們要找的那個元素呢? 答案是:不一定。 如果輸入數(shù)組中存在次數(shù)超過超過數(shù)組長度一半的數(shù),那result就是那個數(shù),否則就不是。所以,我們還需要對這個數(shù)進行檢查,檢查過程請參看下方代碼。

此思路:空間復(fù)雜度O(1),時間復(fù)雜度O(n)。

編碼

  • Java實現(xiàn)
private Object v4_1_solution(Object[] objects) {
    if (objects == null || objects.length < 1) {
        throw new IllegalArgumentException(" array is empty. ");
    }
    // 假設(shè)第一個元素就是超過長度一半的那個
    Object result = objects[0];
    int times = 1;

    // 從第二個元素開始遍歷
    for (int i = 1; i < objects.length; i++) {
        if (times == 0) {
            // 重新設(shè)置
            result = objects[i];
            times = 1;
        } else if (objects[i].equals(result)) {
            times++;
        } else {
            times--;
        }
    }
    if (checkMoreThanHalf(objects, result)) {
        return result;
    } else {
        throw new IllegalArgumentException(" array is invalid ");
    }
}

private boolean checkMoreThanHalf(Object[] objects, Object obj) {
    int times = 0;
    for (int i = 0; i < objects.length; i++) {
        if (objects[i].equals(obj)) {
            times++;
        }
    }
    return times * 2 > objects.length;
}

// 測試類,重點在于實現(xiàn)了equals和hashcode方法。
private static class TestObject {
    String unique;

    public TestObject(String unique) {
        this.unique = unique;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TestObject that = (TestObject) o;

        return unique != null ? unique.equals(that.unique) : that.unique == null;
    }

    @Override
    public int hashCode() {
        return unique != null ? unique.hashCode() : 0;
    }

    @Override
    public String toString() {
        return "TestObject{" +
                "unique='" + unique + '\'' +
                '}';
    }
}
  • C++實現(xiàn)
template <class T>
class Array {
  private:
    bool checkMoreThanHalf(T *objects,unsigned int len,T obj)
 {
     unsigned int times = 0;
     for (int i = 0; i < len; i++) {
         if (objects[i] == obj) {
             times++;
         }
     }
     return times * 2 > len;
 };
  public:
    T v4_1_solution(T *objects,unsigned int len);
};

template <class T>
T Array<T>::v4_1_solution (T *objects,unsigned int len)
{
     if (!objects || len < 1) {
         throw out_of_range(" array is empty. ");
     }
     // 假設(shè)第一個元素就是超過長度一半的那個
     T result = objects[0];
     if(len == 1){
        return result;
     }
     int times = 1;
     // 從第二個元素開始遍歷
     for (int i = 1; i < len; i++) {
         if (times == 0) {
             // 重新設(shè)置
             result = objects[i];
             times = 1;
         } else if (objects[i] == result) {
             times++;
         } else {
             times--;
         }
     }
     if (checkMoreThanHalf(objects,len, result)) {
         return result;
     } else {
         throw out_of_range(" array is invalid ");
     }
}

學(xué)習(xí)心得&解題套路

細心的讀者可能發(fā)現(xiàn)了,文中解題過程大致是這樣的:分析思路->測試用例->編碼->調(diào)試并通過測試。你可能會問怎樣才能很好的掌握算法編程呢?我的建議是:有事沒事刷道題吧。勤加練習(xí),終成大神。哈哈,請輕拍。

  • 關(guān)于解題思路(詳見劍指offer)

    • 畫圖讓抽象問題形象化
    • 舉例讓抽象問題具體化
    • 分解讓復(fù)雜問題簡單化
  • 學(xué)習(xí)資源(信息大爆炸,好資源很重要)

    • 各種數(shù)據(jù)結(jié)構(gòu)及算法書籍: 大話數(shù)據(jù)結(jié)構(gòu)、劍指offer、算法導(dǎo)論等等。
    • 在線編程:LeetCode、??途W(wǎng)七月在線
  • 菜鳥練手推薦:C++在線工具

總結(jié)

現(xiàn)在去大公司面試,都會有算法題,所以不是你想不想掌握它,而是公司會通過它把一部分人淘汰掉,說的可能有點嚇人,但現(xiàn)實就是這樣操作的。文中所有代碼均編譯運行并通過測試用例檢查,由于篇幅限制,代碼沒有貼全,完整的可運行代碼請點擊鏈接獲?。?https://github.com/yangjiantao/DSAA。 由于作者水平有限,文中錯誤之處在所難免,敬請讀者指正。

編程能力就像任何其他技能一樣,也是一個可以通過刻意練習(xí)大大提高的。 --- 摘抄至LeetCode。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,755評論 2 371

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