面試必備:List 演算法

玉剛說發表於2019-03-02

本文首發於微信公眾號「玉剛說」

原文連結:面試必備:List 演算法

題目1:利用陣列實現一個簡易版的List

題目:請利用陣列實現一個簡易版的List,需要實現poll和push兩個介面,前者為移除並獲得隊頭元素,後者為向隊尾新增一個元素,並要求能夠自動擴容。

解題思路

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

List的push和poll過程
List的push和poll過程

注:
(1) 初始化List,陣列預設容量len為8,size=0。(容量小一點方便作圖,實際容量看需求而定。)

(2) 隊尾新增字元 h ,size++。

(3) 新增len-1個字元後,size指向陣列最後一個位置。

(4) 如果再新增字元 O ,由於size++滿足條件:大於等於len,此時需要先對List先擴容,擴容後,再進行新增字元操作。

(5) 接著繼續新增,直到“hello world”都push到List中。

(6) 這是一個poll過程,可以看出即獲取了對頭元素 h ,並且整個陣列中元素向左移動一位來實現移除效果。

關於擴容:每次擴容多少?上圖例子是變為原來的2倍。像ArrayList則是這樣 int newCapacity = oldCapacity + (oldCapacity >> 1),可以看出擴容後大小 = 原來大小 + 原來大小/2。所以擴容多少由你自己決定。

此題關鍵是在怎麼實現poll和push兩個介面上:

push(新增元素):按索引新增到陣列中,size大於等於陣列長度時就先擴容。
poll(獲取並移動對頭元素):移動陣列並置空最後一個元素。

測試用例

  • 功能測試: 新增、移除元素
  • 特殊測試: 新增大量資料(測試擴容)、移除所有元素、null資料

編碼

Java實現

private static final int DEFAULT_CAPACITY = 16;
private Object[] elementData;
// 實際儲存的元素數量
//  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];

    // 陣列元素左移一位 & 最後一位元素置空
    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,具體實現請點選文末原始碼連結前往檢視。
private void ensureExplicitCapacity(int minCapacity) {
    // 期望的最小容量大於等於現有陣列的長度,則進行擴容
    if (minCapacity - elementData.length >= 0)
        grow(minCapacity);
}
複製程式碼

C++實現

class List { 
  private
    int expansionSize = 16;//每次擴容個數
    int elemsSize = 0;//陣列長度
    int dataIndex = -1;//最後一位元素下標
    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&);  // 入棧
    poll();             // 出棧
    int size();
    ~List(){
        if(elemsSize>0){
            delete []elems;
        }
    }
}; 

template <class T>
void List<T>:
:push (T const& elem) 

    if(elemsSize <= 0){//初始化陣列
        elemsSize = expansionSize;
        elems = new T[elemsSize];
    }
    if(dataIndex+1 >= elemsSize){//陣列擴容
        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;
}
複製程式碼

題目2:陣列中出現次數超過一半的數

題目: 一個整數陣列中有一個數字出現的次數超過了陣列長度的一半,請找出這個數字。如輸入一個長度為9的陣列{1,2,3,2,2,2,5,4,2},由於2出現了5次,超過了陣列長度的一半,因此應輸出2。

解題思路

如果我們將陣列排序,那麼排序後位於陣列中間的的數字一定是那個出現次數超過陣列長度一半的數字。這個數就是統計學上的中位數。

此題關鍵在於快速排序演算法,我們一起看看下面這張圖,來理解下快排的思想。

快速排序過程動圖
快速排序過程動圖

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

步驟為:

  • 從數列中挑出一個元素,稱為"基準"(pivot)。
  • 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任何一邊)。在這個分割槽結束之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
  • 遞迴地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。

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

測試用例

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

編碼

Java實現

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];
     // 判斷中位數是否為超過陣列長度一半的數。
     if (checkMoreThanHalf(array, result)) {
         return result;
     } else {
         throw new IllegalArgumentException("not find the number.");
     }
}

public void qSort(int[] arr, int head, int tail) {
    // 引數合法性及結束條件
    if (head >= tail || arr == null || arr.length <= 1) {
        return;
    }
    // 取中間數為基準值
    int i = head, j = tail, pivot = arr[(head + tail) / 2];
    while (i <= j) {
        // 處理大於等於基準數情況
        while (arr[i] < pivot) {
            ++i;
        }
        while (arr[j] > pivot) {
            --j;
        }
        // 直接互換,沒有基準數歸位操作
        if (i < j) {
            swap(arr, i, j);
            ++i;
            --j;
        } else if (i == j) {
            ++i;
        }
    }
    // 遞迴處理基準數分隔的兩個子數列。
    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++ 實現

// 快速排序:遞迴方式 參考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()
{
    //存在出現次數超過陣列長度一半的數字
    //int data[] = {1, 2, 3, 2, 2, 2, 5, 4, 2};
    //不存在出現次數超過陣列長度一半的數字
    //int data[] = {4, 5, 1, 6, 2, 7, 3, 8};
    // 出現次數超過陣列長度一半的數字都出現在陣列的前/後半部分
    int data[] = {222221345};
    //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;
}
複製程式碼

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

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

解題思路

陣列中有一個元素出現的次數超過陣列長度的一半,也就是說它出現的次數比其他所有元素出現次數的和還要多。

因此我們可以考慮在遍歷陣列的時候儲存兩個值: 一個是陣列中的一個元素, 一個是次數。當我們遍歷到下一個元素的時候,如果下一個元素和我們之前儲存的元素相等(equals返回true),則次數加1;如果下一個元素和我們之前儲存的不相等,則次數減1。如果次數為0,我們需要儲存下一個元素,並把次數設為1。由於我們要找的數字出現的次數比其他所有數字出現的次數之和還要多,那麼要找的數字肯定是最後一次把次數設為1時對應的那個元素。

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

陣列中出現次數超過一半的數.png
陣列中出現次數超過一半的數.png

注:雖然途中陣列元素型別是整型,但其思想適用於任何型別。

(1) 陣列初始狀態,times只是一個標記變數,預設為0, result為最後一次設定times=1時的那個元素,預設為NULL。

(2) 開始迴圈,i=0時,times設定為1,並將第一個元素 1 賦值給result變數。

(3) i=1時,由於此時Array[i]的值為 2 ,不等於result,所以times--,操作後times等於0,result不變。

(4) i=2時,由於此時times==0,所以重新設定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,迴圈結束。

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

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

編碼

Java實現

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

    // 從第二個元素開始遍歷
    for (int i = 1; i < objects.length; i++) {
        if (times == 0) {
            // 重新設定
            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;
}

// 測試類,重點在於實現了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++實現

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:
    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. ");
     }
     // 假設第一個元素就是超過長度一半的那個
     T result = objects[0];
     if(len == 1){
        return result;
     }
     int times = 1;
     // 從第二個元素開始遍歷
     for (int i = 1; i < len; i++) {
         if (times == 0) {
             // 重新設定
             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 ");
     }
}
複製程式碼

面試必備:List 演算法
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章