常见排序算法

快排

基本实现: 两侧向中间靠拢的 partition 版本:

int partition(int a[], int left, int right)
{
    int pivot = a[left];  //设置a[left]为主键值
    while(left<right)
    {
        while(left<right && a[right]>=pivot) right--; //从右边开始找到比主键值小的值,移到左边
        a[left]=a[right];
        while(left<right && a[left]<=pivot) left++;   //从左边开始找到比主键值大的值,移到右边
        a[right]=a[left];
    }
    a[left] = pivot;  //跳出while循环后的left==right,此时,区间已经一分为二了,将a[left]的值还原
    return left;
}

/**使用递归快速排序**/
void QuickSort(int a[], int left, int right)
{
    if(left<right)  //快排区间要大于1
    {
        int mid = partition(a,left,right);  //进行一次划分,以a[left]划分区间为左右两个区间
        QuickSort(a,left,mid-1);  //对左区间进行进一步划分
        QuickSort(a,mid+1,right);  //对左区间进行进一步划分
    }
}

/**使用栈的非递归快速排序**/
void qSortNonRecusive(int a[], int left, int right){
    //用栈保存每一个待排序子串的首尾元素下标,while循环时取出这个范围,进行partition操作
    Stack<Integer> st = new Stack<>();
    if(left < right){
        int mid=partition(a,left,right);
        if(left < mid-1){
            st.push(left);
            st.push(mid-1);
        }
        if(mid+1 < right){
            st.push(mid+1);
            st.push(right);
        }
        
        while(!st.isEmpty()){
            right = st.peek(); st.pop();
            left  = st.peek(); st.pop();
            mid=partition(vec,left,right);
            if(left < mid-1){
                st.push(left);
                st.push(mid-1);
            }
            if(mid+1 < right){
                st.push(mid+1);
                st.push(right);
            }       
        }
    }
}

针对数字重复出现的改进: 3-way partition 使用三个指针(两个初始化为开头,一个在结尾), 左中右分别为小于、等于、大于pivot的元素。
另外,可以对pivot的选取随机化:

random_partition(int *a, int left, int right) {
    i = random(left, right);
    swap(a, i, left); // 随机选一个元素当作pivot主元, 放置在开头或末尾的位置.
    return partition(a,left,right);
}

快排空间复杂度

快速排序的原地排序时的空间复杂度是O(log2n)~O(n), 是递归调用的栈空间大小; 最坏情况下递归树的深度是O(n).

最好情况下的时间复杂度

时间复杂度递推关系式:

\[f(n)=2f(n/2)+\theta(n) \]

由主定理得 \(f(n)\in\Theta(n\log n)\).

随机化快速排序的期望时间

假设排序后的数组 A 中的每个元素定义为 z1,z2,z3...zn。这里的 zi 表示为 A 中 第 i 小的元素。定义zi与zj两个元素在排序过程中的比较次数时 \(X_{ij}\), 那么排序的总比较次数为:

\[X = \sum_{i=1}^{n-1} \sum_{j={i+1}}^{n}X_{ij} \]

因此总比较次数的期望为:

\[E[X] = E[\sum_{i=1}^{n-1} \sum_{j={i+1}}^{n}X_{ij}] \\ =\sum_{i=1}^{n-1} \sum_{j={i+1}}^{n}E[X_{ij}] \\ = \sum_{i=1}^{n-1} \sum_{j={i+1}}^{n} P(z_i 比较 z_j) \]

这里我们需要计算zi与zj会发生比较的概率. 由于排序过程中的比较仅出现在pivot与其它元素之间, 因此如果zi与zj发生比较, 那么其中之一必然曾被当作过pivot. 因此 \(P(z_i 比较 z_j)=P(z_i \text{是 pivot}) + P(z_j \text{是 pivot})=2\times {1\over j-i+1}={2\over j-i+1}\)
这里zi与zj离得越远, 两者被随机选择为pivot的概率越小.
因此,

\[E[X] = \sum_{i=1}^{n-1} \sum_{j={i+1}}^{n} P(z_i 比较 z_j) \\ = \sum_{i=1}^{n-1} \sum_{j={i+1}}^{n} {2\over j-i+1} \\ < \sum_{i=1}^{n-1} \sum_{k={1}}^{n} {2\over k} \\ = \sum_{k={1}}^{n} O(\lg n)=O(n\lg n) \]

其中用到了调和级数:

第n个调和级数是: \(H_n=1+\frac{1}{2}+\frac{1}{3}+...+\frac{1}{n}=\sum_{k=1}^n {1\over k}=\ln n+O(1)\)

参考

算法导论. 快排平均时间复杂度.

堆排

基于数组的实现:

// 将最大值移到末尾
void heapSort(int a[],int size)
{
    int heapSize = size;  //heapSize was init to be the length of the array
    buildMaxHeap(a,heapSize); //build a heap on a[]
    for(int i=heapSize;i>0;i--)
    {
        swap(a[0],a[i]); //swap the root of tree with the final element ,get out of the heap
        heapSize --;
        maxHeapify(a,0,heapSize); //重新整队,保持最大堆的性质
    }
}
// 从第一个非叶子节点开始遍历到根节点
void buildMaxHeap(int a[],int size)
{
    for(int k=(size-1)/2;k>=0;k--)  //从第一个非叶子节点开始,遍历到根节点
    {
        maxHeapify(a,k,size);   //进行建堆,就是让所有节点都符合堆的定义 ——根节点都要比子节点大
    }
}
// 从指定节点开始向下
void maxHeapify(int a[],int i,int heapSize)
{
    int left  = (i<<1) + 1; //left child
    int right = (i<<1) + 2; //right child
    int largest =i; //mark the position of the largest element in array
    if(left <=heapSize && a[left] > a[i])
        largest = left;
    else
        largest = i;
 
    if(right <= heapSize && a[right] > a[largest])
        largest = right;
     
    if(largest != i) //说明有节点比根节点大,根节点需要下移
    {
        swap(a[i],a[largest]);//swap 之后需要调整堆
        maxHeapify(a,largest,heapSize); //递归调用,防止子节点违反规则
    }
}

优先级队列

基于大顶堆可实现优先级队列. 主要有以下操作:

  • peek: 返回最大元素, 返回堆顶元素即可
  • poll/extract-max: 移除并返回最大元素, 将堆顶元素与末尾元素交换, 重新maxHeapify
  • set/increase-key: 更改某一元素的值, 如果值增大,那么依次与父节点比较,向上移动到合适位置.
  • offer/add/insert: 添加元素, 在队列末尾新增空元素, 然后调用set/increase-key.
  • PriorityQueue的实现除了这种原始形式的堆, 也可以替换成斐波那契堆FibonacciHeap.

归并排序

/**
    tmp_array[]:辅助数组。
    left_pos:数组左半部分的游标
    left_end:左边数组的右界限
*/
void Merge(int array[], int tmp_array[], int left_pos, int right_pos, int right_end) {
	int i, left_end, num_elements, tmp_pos;
	left_end = right_pos - 1;
	tmp_pos = left_pos;
	num_elements = right_end - left_pos + 1;

	while (left_pos <= left_end && right_pos <= right_end)
		if (array[left_pos] <= array[right_pos])
			tmp_array[tmp_pos++] = array[left_pos++];
		else
			tmp_array[tmp_pos++] = array[right_pos++];
	while (left_pos <= left_end)
		tmp_array[tmp_pos++] = array[left_pos++];
	while (right_pos <= right_end)
		tmp_array[tmp_pos++] = array[right_pos++];
	for (i = 0; i < num_elements; i++, right_end--)
		array[right_end] = tmp_array[right_end];
}

void MSort(int array[], int tmp_array[], int left, int right) {
	int center;
	if (left < right) {
		center = (left + right) / 2;
		MSort(array, tmp_array, left, center);
		MSort(array, tmp_array, center + 1, right);
		Merge(array, tmp_array, left, center + 1, right);
	}
}

void MergeSort(int array[], int n) {
	int *tmp_array;
        // 建立临时数组tmp_array
        tmp_array = (int *)malloc(n * sizeof(int));
	if (tmp_array != NULL) {
		MSort(array, tmp_array, 0, n - 1);
		free(tmp_array);
	}
	else
		cout << "malloc failed" << endl;
}

归并空间复杂度

归并排序的空间复杂度是O(n). 因为归并排序每次递归需要用到一个辅助表,长度与待排序的表相等O(n),递归调用中重复使用这个辅助表。当然, 在一些人的实现当中是动态分配并释放最小量临时空间.  虽然递归次数是O(log2n),但每次递归都会释放掉所占的辅助空间,所以下次递归的栈空间和辅助空间与这部分释放的空间就不相关了,因而空间复杂度还是O(n)。但是动态分配并释放最小量临时空间,那么由malloc/free占用的时间会很多。
空间复杂度为O(1)的归并算法参考: 合并两个有序的子序,要求空间复杂度为O(1)

外存排序-多路归并

有4g大小文件,可用内存1g,如何排序?
1:将大文件分成4个等分小文件分别在内存排序后存入文件。
2:采用四路归并排序。需要注意每路输入和综合输出的内存缓冲区大小,可以等分,即每个200m.输入缓冲区空了之后从文件继续读,输出缓冲满后向最终文件写入。
多路归并: 实现方式-优先级队列
如n路归并, 可以采用固定大小为n的优先级队列. 首先将每路有序数组的首元素加入队列. 然后循环取出队列的最值(从小到大排序则是取最小值, 优先级队列是个小顶堆), 将该最值所在数组中的后一位加入队列中(如果该路数组已经到了末尾则不添加元素了).

多路归并例题 - 序列和的前 k 小元素

给出两个有序表A, B, 求这两个序列中分别任意的元素的和组成的数列中的最小的k个值.

可以转换成多路归并的思想. 元素的和可以组成下列的 n 个有序表, n是A的长度, m 是B的长度:
A[1]+B[1] <= A[1]+B[2] <= ... <= A[1]+B[m]
A[2]+B[1] <= A[2]+B[2] <= ... <= A[2]+B[m]
...
A[n]+B[1] <= A[n]+B[2] <= ... <= A[n]+B[m]
然后我们就可以对这 n 个有序表合并成一个有序表. 并取前k个.
那么怎么样有效的进行前k个元素的多路归并呢?
思路是用一个大小为n的优先级队列, 每次输出一个最小值, 并将其在多路归并的有序表中的位置的后一个元素加入优先级队列.

struct node
{
    int a,b;
    int sum;
    friend bool operator < (node n1 ,node n2)      //priority_queue需要的cmp函数
    {
        return n2.sum < n1.sum;
    }
};
void topKsum(int a[], int n, int b[], int m, int K)  //n路归并求两个序列和过程
{
    node t;
    priority_queue < node , vector< node > > Q;
    for (int i=0;i<n;i++)
    {
        t.a=i;     //t.a表示这个最值来自哪个表:a[i]+b[0],a[i]+b[1],...,a[i]+b[n]
        t.b=0;     //t.b表示当前表已经选到了a[i]+b[t.b](刚开始第一个是0)
        t.sum=a[i]+b[0];
        Q.push(t);
    }
    int result[2005];
    int k=0;
    while(k!=K)
    {
        t=Q.top();
        Q.pop();
        result[k++]=t.sum;           //最小值放入结果序列中
        if(t.b < m-1){                        //多路归并有序表后边还有元素时
            t.b++;                //此表向后推一个
            t.sum=a[t.a]+b[t.b];          
            Q.push(t);             //把当前表的新元素加入到堆中 
        }
        
    }
}

参考 https://www.cnblogs.com/AbandonZHANG/archive/2012/08/06/2625893.html
https://blog.csdn.net/yl1415/article/details/44874487
poj 2442题目 m行n列的组合.

冒泡排序及其改进

在原始冒泡排序的基础上思考改进方案:

  1. 提前有序: 如果某一轮冒泡没有发生交换元素操作说明已有序, 算法停止
  2. 有序区的边界: 原始的冒泡排序是每次冒泡, 最终的有序区的元素数量加一, 但是对于基本有序的情况每次只向有序区增加一个元素还是比较慢. 我们可以在每一轮排序的最后,记录下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。
  3. 如果待排数组的某一侧已经基本有序, 采用一个方向上的冒泡会产生很多次不会发生交换的比较, 如果从两个方向上交替进行冒泡, 比较次数将会进一步减少. 这就是鸡尾酒排序CockTailSort. 实现也比较简单, 不做介绍.
void bubbleSort(int a[]) {
    //记录最后一次交换的位置
    int lastExchangeIndex = 0;
    //无序数列的边界,每次比较只需要比到这里为止
    int sortBorder = array.length - 1;
    for(int i=0; i< a.length; i++) {
        //有序标记,每一轮的初始是true
        boolean isSorted = true;
        for(int j=0; j < sortBorder; j++) {
            if(a[j] > a[j+1]) {
                swap(a, j, j+1);
                //有元素交换,所以不是有序,标记变为false
                isSorted = false;
                //把无序数列的边界更新为最后一次交换元素的位置
                lastExchangeIndex = j;
            }
        }
        sortBorder = lastExchangeIndex;
        if(isSorted) break;
    }
}

常用功能

swap 不借助变量交换两个数

为swap函数增加判断语句:当a和b相等的时候不交换。否则下面两种方法会导致a的结果为0.

//A方法, 容易溢出
void swap(int &a, int &b) {
    if (a != b) {
        a = a + b;
        b = a - b;
        a = a - b;
    }
}
//B方法
void swap(int &a, int &b) {
    if (a != b) { 
        a ^= b; 
        b ^= a;
        a ^= b;
    }
 }

二分查找/上下界

Java中有二分的实现,叫做java.util.Arrays.binarySearch()。使用二分的前提是数组必须有序(从小到大)。如果没有排序,那么方法无法确定返回哪个值。对于有序的数组,如果数组中包含多个相同的目标值,方法也无法保证找到的是哪一个。若找到了目标值,方法会返回目标值所在的下标;如果没有找到目标值,则方法会返回一个可以插入该值的位置,以负数表示 。

int binarySearch(int[] nums, int target){
    int s = 0;
    int e = nums.length - 1;
    while(s <= e){
        int mid= s + ((e-s)>>1);
        if(nums[mid]==target) return mid; //return true;
        else if(nums[mid]>target){
            e=mid-1;
        }else s=mid+1;
    }
    return -(s+1); //return false;
 }

实现lower_bound 和 upper_bound

// 返回第一个大于等于value的下标, 注意数组范围是[begin, end-1]
int lower_bound(int[] nums, int begin, int end, int value) {
    while (begin < end) {
        int mid = begin + (end - begin) / 2;
        if (nums[mid] < value) {
            begin = mid + 1;
        } else {
            end = mid;
        }
    }
    return begin;
}
// 返回第一个大于value的下标.
int upper_bound(int[] nums, int begin, int end, int value) {
    while (begin < end) {
        int mid = begin + (end - begin) / 2;
        if (nums[mid] <= value) {
            begin = mid + 1;
        } else {
            end = mid;
        }
    }
    return begin;
}

另可参考C++ STL实现 https://sumygg.com/2017/09/08/upper-bound-and-lower-bound-in-java/

posted @ 2018-07-30 17:32  康行天下  阅读(378)  评论(0编辑  收藏  举报