Sorting Algorithm

选择排序

排序思想:每次挑选最值(最大值或者最小值)然后顺序放在前面(或者后面)以达到升序(或者降序)的排序。以升序为例,从第一个开始挑选最小的放在第一位,然后再从除了第一个之后的所有元素挑选最小的,放在第二位,以此类推。

复杂度解释:显然从第一个开始挑选到最后一次挑选需要一个循环,每次挑选过程都要遍历剩下的元素,因此内嵌一个循环。算法复杂度时O(n^2)

template<class T>
void select_sort(std::vector<T> &v){
    for (int i = 0; i<v.size(); i++) {
        int min_index = i;
        for (int j = 0; j<v.size(); j++) {
            if (v[min_index]<v[j]) {
                std::swap(v[min_index], v[j]);
            }
        }
    }
}

冒泡排序

排序思想:大的元素不断上升或者小的元素不断沉底。“不断”的过程可以用两个循环来实现。两两比较如果满足条件就交换。

template<class T>
void bubble_sort(std::vector<T> &v){
    for (int i = 0; i<v.size(); i++) {
        for (int j = i; j<v.size()-i-1; j++) {
            if (v[j+1]<v[j]) {  //if next one is smaller then swap
                std::swap(v[j], v[j+1]);
            }
        }
    }
}

插入排序

排序的思想:类似于扑克牌的整理,依次挑选并与之前的比较,插入到合适的位置。由于插入到之前的位置需要把该元素之前的部分元素依次后移,所以需要一个while循环来进行迭代赋值。然后最后再把要插入的元素的值覆盖到所在的位置。
注意,因为要表示下一个元素,用到i+1,所以循环要改成v.size()-1以保证不越界。同时为了避免空数组的v.size()-1<0的情况发生,还需要一个简单的if判断。

template <class T>
void insert_sort(std::vector<T> &v) {
    if (v.size() > 1) {
        for (int i = 0; i<v.size()-1; i++) {
            T next_value = v[i+1];
            int next = i+1;
            while(v[next-1] > next_value ){ //if next one is smaller than previous
                v[next] = v[next-1]; //overwrite the next one with privous
                next--;
            }
            v[next] = next_value;
        }
    }
}

快速排序

排序思想:不断划分小块。如果在升序的情况,则划分点左边的元素值均小于划分点的值,划分点右边的值均大于划分点的值。可以先选择最后一个元素作为划分点,然后陆续遍历一次把前面所有元素和划分点比较,设置一个用来记录比划分点小的index,当遇到比划分点小的值时用来交换。遍历结束后把划分点移动到index的前面,这样就保证了左边小于划分点,右边大于划分点。一次partition的过程可以用下图说明:

template <class T>
int partition(std::vector<T> &v, int first, int last){
    T pivot = v[last];
    int i = first - 1;
    for (int j = first; j<v.size()-1; j++) {
        if (v[j]<pivot) { //如果当前值小于pivot的值
            i++; 
            std::swap(v[i], v[j]);
        }
    }
    std::swap(v[i+1], v[last]); //最后把pivot指的元素放在最后一个比它小的元素的前面
    return (i+1);
}

template <class T>
void quick_sort(std::vector<T> &v, int first, int last) {
    if (first<last) {
        int pi = partition(v, first, last);
        quick_sort(v, first, pi-1); 
        quick_sort(v, pi+1, last); 
    }
}

归并排序

排序思想:最后一种常用的排序是归并排序,其代码会稍长,但思路是比较容易理解的。
首先主函数每次迭代都需要计算一个新的中点,就是为了不断切分成小块,迭代地调用自身,并传入左半部分和右半部分进去。与快排不一样,快排是在分块的时候排序,而归并是在合并的时候排序,因此merge函数是在两个自身递归调用的下面。merge函数内容分为以下几个步骤:

  1. 创建两个vector,分别存放左半部分[first, middle-first+1]和右半部分 [middle, last-middle]
  2. 依次把原数组的firstmiddle-first+1的元素复制到左半部分
  3. 依次把原数组的middlelast-middle的元素复制到右半部分
  4. i指向左半部分第0个,j指向右半部分第0个,当ij都不超过各自数组时,k指向原数组要被覆盖的第0个,也就是k=first。循环比较把小的重新覆盖到原数组去
  5. 然后再用while循环把剩下的复制到原数组去
  6. 结束
template <class T>
void merge(std::vector<T> &v, int first, int middle, int last) {
    int i, j, k;
    int n1 = middle-first+1;
    int n2 = last - middle;
    std::vector<T> v1(n1);
    std::vector<T> v2(n2);
    
    for (i = 0; i< n1; i++)
        v1[i] = v[first + i];
    
    for (j = 0; j<n2; j++)
        v2[j] = v[middle+1+j];
    
    i = 0;
    j = 0;
    k = first;
    
    while (i<n1 && j < n2) {
        if (v1[i]<v2[j]){
            v[k] = v1[i];
            i++;
        }
        else {
            v[k] = v2[j];
            j++;
        }
        k++;
    }
    
    while(i<n1){
        v[k] = v1[i];
        i++;
        k++;
    }
    
    while(j<n2){
        v[k] = v2[j];
        j++;
        k++;
    }
}

template <class T>
void merge_sort(std::vector<T> &v, int first, int last){
    if (first < last) {
        int middle = first + (last - first)/2;
        merge_sort(v, first, middle);
        merge_sort(v, middle+1, last);
        merge(v,first, middle, last);
    }
}

计数排序

排序思想: 通过一个计数的数组来存放各个元素应该在的位置,然后在重新拷贝到结果数组去。这里需要获得一个最大值。比如要排序[1 4 1 2 7 5 2],则需要知道最大值是7,这样才可以创建一个有7个元素的计数数组。弊端是空间的损失,如果最大值很大,则要创建的计数数组也很大。

template <class T>
void count_sort(std::vector<T> &v, int max) {
    
    //create a count vector
    std::vector<int> count(max+1);
    for (int i = 0; i < v.size(); i++) {
        count.at(v[i]) ++;
    }
    
    //self increment of count vector
    for (int i = 1; i < count.size(); i++) {
        count[i] += count[i-1];
    }
    //create result vector
    std::vector<int> result(v.size());
    
    //put the element in the correct position in result vector
    for (int i = 0; i<v.size(); i++) {
        int current_index = count[v[i]]-1;
        result[current_index] = v[i];
        count[v[i]] --;
    }
    
    //copy the result to the input vector
    v=result;
}

堆排序

堆排序中,因为二叉堆是一个完全二叉树,因此我们可以使用数组的形式来表示二叉堆。否则就要用一些符号来填充数组某些位置以表示空节点。
数组表示的二叉堆,设双亲位置为ii从0开始
则左孩子的位置为2*i + 1
且右孩子的位置为2*i + 2
最大堆就是双亲>左右孩子最小堆就是双亲< 左右孩子
升序排序用最大堆,降序排序用最小堆。

def heapify(arr, n, i):
    # 先把parent设为最大的,parent的位置为i
    largest = i  
    # 然后根据公式 写出左右child 的位置 2i+1, 2i+2
    l = 2 * i + 1     
    r = 2 * i + 2     
 
    # 如果左孩子存在,也就是l< n 且左孩子的值比largest的位置的值大
    # 则设最大值的位置为左孩子的位置
    if l < n and arr[largest] < arr[l]:
        largest = l
     
    # 同理判断右孩子存在与否 并和largest位置的值比较
    if r < n and arr[largest] < arr[r]:
        largest = r
 
    # 如果largest的位置和parent的位置不一样,证明最大值不在parent那里
    # 则将largest位置的元素和parent位置的元素交换
    # 并且继续递归调用
    if largest != i:
        arr[i], arr[largest] = arr[largest], arr[i]  # swap
        heapify(arr, n, largest)
        
def heap_sort(arr):
    n = len(arr)
 
    # 对于数组表示的二叉堆,能作为parent的只有前 n//2 -1 个元素
    # 应该从最下面的parent开始进行最大堆化,
    # 这样才能保证循环完整个二叉堆都是最大堆
    # 可以由不等式的方式说明
    for i in range(n//2-1, -1, -1): 
        heapify(arr, n, i)
    
    # 建立完二叉堆之后就是顺序取出二叉堆的元素即可得到有序数组
    # 方法把第一个放到最后,然后固定最后的再把前面的最大堆化
    for i in range(n-1, 0, -1):
        arr[i], arr[0] = arr[0], arr[i]   
        heapify(arr, i, 0)

留下评论