排序算法分析及实现C++

参考资料:
经典排序算法过程经典排序算法性质、《王道数据结构》、严蔚敏《数据结构》

概念

  1. 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  2. 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  3. 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  4. 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
    在这里插入图片描述

蛮力

简单选择排序

  • 思想:每一趟扫描后面n-i+1个待排序元素,选取其中的最小元素,作为有序子序列的第i个元素,直到第n-1趟结束
void SelectSort(int A[], int n){
        for(int i=0; i<n-1; i++){
            int minpos = i; // 记录最小元素位置,最终放到i处
            for(int j=i+1; j<n; j++){
                if(A[j]<A[minpos]) minpos = j;
            }
            if(minpos!=i) swap(A[i], A[minpos]); // 与第i个元素交换位置
        }
    }

  • 时间效率:最好的就是已经排好序的,移动0次,需要元素比较n(n-1)/2次,所以时间复杂度为O(n2)。

冒泡排序

  • 思想:两两比较相邻元素的值,若为逆序(A[i-1]>A[i] 若想升序排列),则交换它们,直到序列比较完,完成一趟冒泡,把序列中最小的放到了待排序列的前面,下一次冒泡时前面已经确定的最小元素不参与。最多n-1次冒泡就能排好。
void BubbleSort(int A[], int n){// 升序
	for(int i = 0; i < n-1; i++){ // n-1趟排序就OK
		bool flag = false; // 本次排序是否发生交换
		for(int j = n-1; j>i; j--){ // 一趟冒泡排序
			if(A[j-1]>A[j]){
				swap(A[j-1],A[j]);
				flag = true;
			}
			if(flag==false) return;  // 本次遍历没有发生交换,说明已经OK了
		}
	}
}
  • 空间效率:只用了常数个辅助单元O(1)
  • 时间效率:
    • 最好是已经完全排序,比较n-1次,移动0次。
    • 最坏是完全逆序,n-1趟排序,第i趟需要n-i次关键字比较,每次比较移动3次来交换元素位置。O(n2).
    • 平均O(n2)

减治法

插入排序

  • 思路:取出未排序的数列第一个数,插入已排序的数列中的恰当位置,
    再从余下来未排序的数列中的第一个数取出,放入。
  • 时间:全升序是最好的:O(n);全降序是最差的:O(n2),平均O(n2)
  • 适用于顺序存储和链式存储的线性表
// 直接插入排序
void InsertSort(int* h, size_t len){
    if(h==NULL) return;
    if(len<=1) return;
    int i,j;
    //i是次数,也即排好的个数;j是继续排
    for(i=1;i<len;++i) 
        for(j=i;j>0;--j) // h[j]元素去前面找它该去的位置
            if(h[j]<h[j-1]) swap(h[j],h[j-1]);
            else break;
    return;
}

希尔排序

  • 思路:跳跃式分组,对每个组做插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
  • 在特定范围内,时间复杂度为O(n1.3),最坏情况下为O(n2)。
  • 不稳定,相同关键字分到不同子表,可能会改变它们之间的相对次序。
  • 希尔排序只适用于线性表为顺序存储的情况。
//希尔排序
void ShellSort(int* h, size_t len){
    if(h==NULL) return;
    if(len<=1) return; 
    for(int div=len/2;div>=1;div/=2) // 步长div变化
        for(int k=0;k<div;++k) // 形成了div个分组,对每一组做如下操作:
            for(int i=div+k;i<len;i+=div) // 对一个分组进行插入排序
                for(int j=i;j>k;j-=div) // 去这个分组里前面找它的位置
                    if(h[j]<h[j-div]) swap(h[j],h[j-div]);
                    else break;              
    return;
}

拓扑排序

  • 拓扑排序是个有向无环图,每个顶点出现且只出现1次,若A→B,则序列中A在前,B在后。只有DAG才有拓扑排序。
  • DFS:执行DFS遍历,写入堆栈中;然后出栈顺序即逆序;边遇到回边,不返回
  • 减治技术:找出源(没有输入边的顶点)→把它和所有从它出发的点删除→顶点被删的次数就是解。

分治法

快排

  • 思想: left指针,right指针,key参照数(分水岭)

    • 当左指针位置 < 右指针:
      • 右指针从右往左遍历,遇到小的就赋值给left。
      • 而后左指针向右遍历,遇到大的就赋值给right。而后右指针左遍历同理。
    • 最终左右指针重叠:让key左边的都<她,右边的都>她
    • 然后递归左右两边。
  • 过程:以20 40 50 10 60 为例

    • (1)从left取出数字 (20) 作为基准参照物。
    • (2)从数组right位置向前找,找到< key的数,则将该数赋值给left,此时数组为10 40 50 10 60,左右指针指向10
    • (3)从left向后找,一直找到比(key)大的数,如果找到,将此数赋给right的位置(也就是40赋给10),此时数组为:10,40,50,40,60, left和right指针分别为前后的40。
    • (4)重复2,3步骤直到俩指针重合,最后将(key)放到40的位置, 此时数组值为: 10,20,50,40,60,至此完成一次排序。
    • (5)此时20已经潜入到数组的内部,20的左侧一组数都比20小,20的右侧作为一组数都比20大, 以20为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行,最终快排大功告成。
void QuickSort(int A[], int left, int right){
	if(left < right){
		int pivotpos = Partition(A, left, right); // key的位置为分水岭
		QuickSort(A, left, pivotpos-1); // 递归
		QuickSort(A, pivotpos+1, right);
	}
}

int Partition(int A[], int left, int right){
	int key = A[left]; // 设定当前数组最左元素为基准元素
	while(left < right){
		while(left<right && A[right]>=key) --right;
		if(left < right) A[left] = A[right];
		while(left<right && A[left]<key) ++left;
		if(left < right) A[right] = A[left];
	}
	A[left] = key; // 把基准元素放到它最终位置
	return left; // 返回基准元素的位置,用来划分下面的两个子数组
}
  • 不稳定算法:右端区间有俩值一样,且都<key,那么他俩在交换到左边后,相对位置是变化了的。
  • 空间效率:递归工作栈,容量和递归调用的最大深度一致,
    • 最好 ⌈ log ⁡ 2 ( n + 1 ) ⌉ \left\lceil \log _{ 2 }{ \left( n+1 \right) } \right\rceil log2(n+1)
    • 最坏要进行n-1次递归,栈的深度是O(n),
    • 平均,深度是 O ( log ⁡ 2 n ) O\left( \log _{ 2 }{ n } \right) O(log2n)
  • 时间效率:快排的运行时间和划分是否对称有关:
    • 最坏:两边n-1个和0个,即初始排序基本有序或逆序, O ( n 2 ) O\left( { n }^{ 2 } \right) O(n2),比较次数 n − 1 + n − 2 + … + 1 = n ( n − 1 ) 2 ∼ n 2 2 n-1 + n-2 +…+1 =\frac { n\left( n-1 \right) }{ 2 } \sim \frac { { n }^{ 2 } }{ 2 } n1+n2++1=2n(n1)2n2
    • 最好:每次都正好中分 T ( n ) = O ( n log ⁡ 2 n ) T\left( n \right) =O\left( { n }\log _{ 2 }{ n } \right) T(n)=O(nlog2n),一次递归比较n次,递归深度 l o g 2 n log_{2}{n} log2n
    • 平均: 推导过程
      在这里插入图片描述
      注:ln( n ) > 1/2 + 1/3 … + 1/n
      在这里插入图片描述
  • 若是随机选取起点,则代码如下:
// 快速排序,随机选取哨兵放前面 
void QuickSort(int* h, int left, int right)
{
    if(h==NULL) return;
    if(left>=right) return;
    
    //防止有序队列导致快速排序效率降低 
    srand((unsigned)time(NULL));
    int len=right-left;
    int kindex=rand()%(len+1)+left;
    Swap(h[left],h[kindex]);
    
    // 也可以直接用数组的最左值作为基准点
    int key=h[left],i=left,j=right;
    while(i<j) // 左指针位置<右指针则可以继续这趟遍历
    {
        while(h[j]>=key&&i<j) --j; // 若右边的值大于key,则右指针持续向左遍历
        if(i<j) h[i]=h[j]; // 当遇到小的了,赋值给左指针指向的位置
        while(h[i]<key&&i<j) ++i;// 若左边的数小,左指针向右遍历
        if(i<j) h[j]=h[i]; // 当遇到大的了,就赋值给右边指向的位置
    }
    // 俩指针重叠后,此轮遍历结束,找到key的最终位置
    h[i]=key;
    // 继续对两边的子数组同理操作(递归)
    QuickSort(h,left,i-1);
    QuickSort(h,j+1,right);
}

归并排序

  • 思路:先对半分(分到最后只有单个对单个的元素);然后两个有序数组合并。
// 归并排序-递归
void Merge(vector<int> A, int low, int mid, int high){
    vector<int> B(A);
    int i=low,j=mid+1,k=i;
    while(i<=mid&&j<=high){
        if(B[i]<=B[j]){ A[k]=B[i++];} // 比较B左右两段子数组的元素,较小的值复制到A
        else{ A[k]=B[j++];}
        k++;
    }
    while(i<=mid) A[k++]=B[i++]; // 有一边遍历完了,剩下的都直接放到A里。
    while(j<=high)A[k++]=B[j++];
}

void MergeSort(vector<int>A, int left, int right){
    if(left<right){
        int mid = left+(right-left)/2;
        MergeSort(A,left,mid);
        MergeSort(A,mid+1,right);
        Merge(A,left,mid,right);
    }
}

非递归法详见LeetCode题解

变治法

预排序

  • 检验数的唯一性,预先排序再比较O(nlogn)
  • 模式计算:先排序,再选出相邻次数最多的等值即可。

堆排序(树形选择排序)

  • 堆:
    1. 视为完全二叉树的顺序存储结构。每个节点都比其左右子树大(最大堆、大根堆)或小(最小堆、小根堆)。
    2. 从根节点到任意结点路径上的结点序列都有序。
  • 堆的操作:
    1. 【插入】 T ( n ) = O ( l o g 2 n ) T(n)=O(log{_2}{n}) T(n)=O(log2n) 插到尾部,再交换调节,保持有序
    2. 【删除】
      取出根节点元素,同时删除堆的结点a,让a去替换原来的根节点;
      然后找出新根节点的较大孩子,与新的根比较大小,更大的作为根节点;若交换了,再比较a的子树。O(logn)
    3. 【建堆】最坏的情况下需要移动元素次数 = 树中各节点下沉高度和 O(N) ,即:叶结点下沉高度为0
  • 堆排序思想:
    输出堆顶元素(最值)后,将剩余的n-1个元素序列重新建成堆,则可得到次大(小)值,反复执行,直到只剩1个元素。
  • 堆排序:
    注:最后一个结点是第 ⌊ n 2 ⌋ \left\lfloor \frac { n }{ 2 } \right\rfloor 2n个结点a的孩子。对a为根的子树进行筛选,使得该子树成为堆。之后,向前依次对以前一半结点为根的子树进行筛选。
    在这里插入图片描述
  • 空间效率:O(1)
  • 建立大根堆:时间复杂度O(n) (向下调整时因为大部分结点高度较小),后面有n-1次向下调整的操作,每次调整时间复杂度为O(h)
  • 时间复杂度:先构造堆,再删除最大键,降序删除,好坏平均都是:O(nlogn)
  • 堆排序总体代码实现:
    注:父节点是k,左子结点是2k+1,右子节点是2k+2
// 堆排序-向下调整
void moveDown(vector<int>& array, int first, int last){
    int curIndex = first * 2 + 1; // first的左子节点索引
    while (curIndex <= last){
        // 找first为根节点的左右两节点(若存在)更大的那个,将其脚标赋值给curIndex
        if (curIndex < last && array[curIndex] < array[curIndex + 1]){
            curIndex++;
        }
        // 将更大的子节点与根节点比较,更大的置于根节点。即:若根节点值小于子节点值,则交换
        if (array[first] < array[curIndex]){
            swap(array[first], array[curIndex]);
            first = curIndex; // 还需要继续向下看交换后,根节点的孙子树等是否受到影响
            curIndex = first * 2 + 1;
        }
        else{ // 若根节点大,那就不必调整了。
            break;
        }
    }
}
// 从最下面的部分开始,把它和子树调整成堆的形式;
// 然后沿着数组往前(堆往上的一层)调整成更大的堆。
void buildHeap(vector<int>& array){ // 用数组实现堆
    int arrsize = (int)array.size();
    for(int i = arrsize/2 - 1; i>=0; i--){ // 最后一个非叶节点的节点索引
        moveDown(array, i, arrsize-1);
    }
}
// 堆排序
void heapSort(vector<int>& array){
    // 生成堆
    buildHeap(array);
    // 堆顶、底索引
    int first = 0, last = (int)array.size() - 1;
    while (first <= last){
        swap(array[first], array[last]); // 堆顶和堆底元素交换,堆顶元素坠到数组尾端
        moveDown(array, first, --last);  // 整理,把剩余的i-1个元素重新整理成堆
    }
}
  • 图解
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

计数排序

基数排序

  • 采用多关键字排序思想(基于关键字各位的大小进行排序),不需要比较关键字的大小。
  • 分为MSD(最高位优先)和MLD(最低位优先:从个位数开始排序,而后是十位数等等)
  • 空间效率:一趟排序的辅助存储空间:r 个队列,以后会重复使用。
    • (关键字的值∈[0,r-1],比如数字是0~9)
  • 时间效率:n个结点,需要d趟分配和收集,一趟分配需要O(n),一趟收集需要O( r),时间复杂度为:O(d(n+r)),与序列的初始状态无关
    • (每个结点的关键字由d元组组成,比如:数字是d位数)
  • 稳定的。
  • 适合字符串和整数这种有明显结构特征的关键码。

桶排序

  • 将待排序集合中处于同一值域的元素放入一个桶中,拆分后形成多个桶,对每个桶中的元素进行排序(可任选排序方法)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值