面试必备:List 算法

1,940 阅读18分钟

本文首发于微信公众号「玉刚说」

原文链接:面试必备: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 ");
     }
}

欢迎关注我的微信公众号「玉刚说」,接收第一手技术干货