C++面试总结之算法(一):排序

排序有几种方式,分别介绍其实现原理?(问)

(1)冒泡排序

void bubble_sort(int arr[], int len) {  
    int i, j;  
    for (i = 0; i < len; i++) 
        for (j = len-1; j > i; j--) 
            if (arr[j] > arr[j - 1])   
                swap(arr[j], arr[j - 1]);  
} 

(2) 选择排序

void selection_sort(int arr[], int len) {  
    int i, j, min;      
    for (i = 0; i < len - 1; i++) {  
        min = i;  
        for (j = i + 1; j < len; j++)  
            if(arr[min] > arr[j])  
                min = j;  
        if(min != i)
            swap(arr[i], arr[min]);  
    }  
} 

 

(3) 快排(挖坑排序)(重要)

1. 右指针找比基准数小的,左指针找比基准数大的,交换之

2. 冒泡+二分+递归分治

void quickSort(int s[], int left, int right)  {  
    if (left < right)   {        
        int i = left, j = right, x = s[left];  
        while (i < j)   {  
            while(i < j && s[j]>= x) // 从右向左找第一个小于x的数  
                j--;   
            if(i < j)  
                s[i++] = s[j];  
            while(i < j && s[i]< x) // 从左向右找第一个大于等于x的数  
                i++;   
            if(i < j)  
                s[j--] = s[i];  
        }  
        
        s[i] = x;  
        quickSort(s, left, i - 1); // 递归调用  
        quickSort(s, i + 1, right);  
    }  
}  

为什么一定要j指针先动呢?首先这也不是绝对的,这取决于基准数的位置,因为在最后两个指针相遇的时候,要交换基准数到相遇的位置。一般选取第一个数作为基准数,那么就是在左边,所以最后相遇的数要和基准数交换,那么相遇的数一定要比基准数小。所以j指针先移动才能先找到比基准数小的数。

(4) 插入排序

插入排序是通过比较找到合适的位置插入元素来达到排序的目的。举个栗子,对5,3,8,6,4这个无序序列进行简单插入排序,首先假设第一个数的位置时正确的,想一下在拿到第一张牌的时候,没必要整理。然后3要插到5前面,把5后移一位,变成3,5,8,6,4.想一下整理牌的时候应该也是这样吧。然后8不用动,6插在8前面,8后移一位,4插在5前面,从5开始都向后移一位。注意在插入一个数的时候要保证这个数前面的数已经有序。

void insertion_sort(int arr[], int len) { 
    int i, j, temp;  
    for (i = 1; i < len; i++) {  
        temp = arr[i];  
        for (j=i-1; j>=0 && arr[j]>temp; j--)  //一直后移
            arr[j+1] = arr[j];  
        arr[j+1] = temp;  
    }  
}  

(5) 希尔排序(分组+直接插入)

/*希尔排序:先将整个待排元素序列分割成若干子序列(由相隔某个“增量”的元素组成的)
分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序
(增量足够小)时,再对全体元素进行一次直接插入排序(增量为1)。其时间复杂度
为O(n^3/2),要好于直接插入排序的O(n^2) */

void Shell_sort(int a[],size_t n)  {
    int i, j, k, gap;
    for (gap = n/2; gap > 0; gap /= 2) { //增量序列为n/2,n/4.直到1
        for (i = 0; i < gap; ++i) {
            for (j = i+ gap; j < n; j += gap) { //对每个分组插入排序
                if (a[j - gap] > a[j]) { //对a[j]进行插入排序
                    int temp = a[j];
                    k = j - gap;

                    while (k>=0 && a[k]>temp) {
                        a[k+ gap] = a[k];
                        k -= gap;
                    } 

                    a[k+gap] = temp;
                } 
            }
        }
    }
}

(6)归并排序


 

/*假设初始序列含有n个记录,则可以看成n个有序的子序列,每个子序列的长度为1,
然后两两归并,得到(不小于n/2的最小整数)个长度为2或1的有序子序列,再两两归并,
...如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序.
时间复杂度为O(nlogn),空间复杂度为O(n+logn),如果非递归实现归并,则避免了递
归时深度为logn的栈空间, 空间复杂度为O(n) */

/*lpos is the start of left half, rpos is the start of right half*/

void merge(int a[], int tmp_array[], int left_start, int right_start, int right_end){
    int i, left_end, num_elements, tmpos;
    left_end = right_start - 1;
    tmpos = left_start;
    num_elements = right_end – left_start + 1;
    while (left_start <= left_end && right_start <= right_end)
        if (a[left_start] <= a[right_start])
            tmp_array[tmpos++] = a[left_start++];
        else
            tmp_array[tmpos++] = a[right_start++];

    while (left_start <= left_end) /*copy rest of the first part*/
        tmp_array[tmpos++] = a[left_start++];
    while (right_start<=right_end)/*copy rest of the second part*/
        tmp_array[tmpos++] = a[right_start++];
    /*copy array back*/
    for (i = 0; i < num_elements; i++, rightn--)
        a[right_end] = tmp_array[right_end];
}


void msort(int a[], int tmp_array[], int left, int right){
    int center;
    if (left < right){
        center = (right + left) / 2;
        msort(a, tmp_array, left, center);
        msort(a, tmp_array, center + 1, right);
        merge(a, tmp_array, left, center + 1, right);
    }
}


void merge_sort(int a[], int n){
    int *tmp_array;
    tmp_array = (int *)malloc(n * sizeof(int));
    if (tmp_array != NULL){
        msort(a, tmp_array, 0, n - 1);
        free(tmp_array);
    }
    else {
        free(tmp_array);
        printf("No space for tmp array!\n");
    }
}

非递归实现

void merge_sort(int arr[], int len) {  
    int* a = arr;  
    int* b = new int[len];  

    for (int seg = 1; seg < len; seg += seg) {  
        for (int start=0; start<len; start += seg+seg) {  
            int low=start, mid = min(start+seg, len); 
            int high = min(start+seg+seg, len), k = low;  
            int start1 = low, end1 = mid;  
            int start2 = mid, end2 = high;  

            while (start1 < end1 && start2 < end2)  
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];  
            while (start1 < end1)  
                b[k++] = a[start1++];  
            while (start2 < end2)  
                b[k++] = a[start2++];  
        }  

        int* temp = a;  
        a = b;  
        b = temp;  
    }  

    if (a != arr) {  
        for (int i = 0; i < len; i++)  
            b[i] = a[i];  
        b = a;  
    }  
    delete[] b;  
}  

(7)堆排序

堆排序的基本思想就是:从最大(小)堆得顶部不断取走堆顶元素放到有序序列中,直到堆的元素被全部取完。堆排序完全依赖于最大(小)堆的相关操作。

A. 堆的插入

(1) 将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列.

(2) 将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中.

//新加入i结点,其父结点为(i - 1) / 2,调整堆

void MinHeapFixup(int a[], int i) {
    int j = (i-1)/2, temp = a[i]; //父结点  

    while (j >= 0 && i != 0) {
        if (a[j] <= temp)
            break;
        a[i] = a[j];     //把较大的子结点往下移动,替换它的子结点
        i = j;  j = (i - 1) / 2;
    }
    a[i] = temp;
}

B. 堆的删除

按定义,堆中每次都只能删除第0个数据。为了便于重建堆,实际的操作是将最后一个数据的值赋给根结点,然后再从根结点开始进行一次从上向下的调整。调整时先在左右儿子结点中找最小的,如果父结点比这个最小的子结点还小说明不需要调整了,反之将父结点和它交换后再考虑后面的结点。相当于从根结点将一个数据的“下沉”过程。

//  从i节点开始调整,  n为节点总数,从0开始计算

void MinHeapFixdown(int a[], int i, int n) {
    int j = 2 * i + 1, temp = a[i];

    while (j < n) {
        if (j + 1 < n && a[j + 1] < a[j]) //在左右孩子中找最小的
            j++; 
        if (a[j] >= temp)
            break; 
        a[i] = a[j];     //把较小的子结点往上移动,替换它的父结点
        i = j;  j = 2 * i + 1;
    }
    a[i] = temp;
}

C.堆化数组

有了堆的插入和删除后,再考虑下如何对一个数据进行堆化操作。要一个一个的从数组中取出数据来建立堆吧,不用!先看一个数组,如下图:

很明显,对叶子结点来说,可以认为它已经是一个合法的堆了即20,60, 65, 4, 19都分别是一个合法的堆。只要从A[4]=50开始向下调整就可以了。然后再取A[3]=30,A[2] = 17,A[1] = 12,A[0] = 9分别作一次向下调整操作就可以了。下图展示了这些步骤:

写出堆化数组的代码:

//建立最小堆

void MakeMinHeap(int a[], int n) {
    for (int i = n / 2 - 1; i >= 0; i--)
        MinHeapFixdown(a, i, n);
}

D.堆排序

首先可以看到堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。

void MinheapsortTodescendarray(int a[], int n){
    for (int i = n - 1; i >= 1; i--){
        Swap(a[i], a[0]);
        MinHeapFixdown(a, 0, i);
    }
}

由于堆也是用数组模拟的,故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。

注意使用最小堆排序后是递减数组,要得到递增数组,可以使用最大堆。

由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度为O(N * logN)。

(8)计数排序

对已知范围的应用排序。而且这个范围不能太大。它创建一个长度为这个数据范围的数组C,C中每个元素记录要排序数组中对应记录的出现个数。

假设要排序的数组为 A = {1,0,3,1,0,1,1}这里最大值为3,最小值为0,那么我们创建一个数组C,长度为4.然后一趟扫描数组A,得到A中各个元素的总数,并保持到数组C的对应单元中。比如0 的出现次数为2次,则 C[0] = 2;1 的出现次数为4次,则C[1] = 4

由于C 是以A的元素为下标的,所以这样一做,A中的元素在C中自然就成为有序的了,这里我们可以知道 顺序为 0,1,3 (2 的计数为0)然后我们把这个在C中的记录按每个元素的计数展开到输出数组B中,排序就完成了。也就是 B[0] 到 B[1] 为0  B[2] 到 B[5] 为1 这样依此类推。

这种排序算法,依靠一个辅助数组来实现,不基于比较,算法复杂度为 O(n) ,但由于要一个辅助数组C,所以空间复杂度要大一些,由于计算机的内存有限,这种算法不适合范围很大的数的排序。

注:基于比较的排序算法的最佳平均时间复杂度为 O(nlogn)

void Sort(int[] A, int k) {
    int[] C = new int[k + 1];
    for (int j = 0; j < A.Length; j++)
        C[A[j]]++; 
    int z = 0;
    for (int i = 0; i <= k; i++){
        while (C[i]-- > 0)
            A[z++] = i;
    }
}

由于C数组下标 i 就是A 的值,所以我们不需要保留A中原来的数了,这个代码减少了一个数组B,而且要比原来的代码简化了很多。

(9)基数排序

按位数从最低位开始排序

参考:https://github.com/francistao/LearningNotes/blob/master/Part3/Algorithm/Sort/%E9%9D%A2%E8%AF%95%E4%B8%AD%E7%9A%84%2010%20%E5%A4%A7%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E6%80%BB%E7%BB%93.md

http://www.cnblogs.com/weixliu/archive/2012/12/23/2829671.html

https://blog.csdn.net/jnu_simba/article/details/9705111

猜你喜欢

转载自blog.csdn.net/lxin_liu/article/details/89306496
今日推荐