数据结构—图解排序算法+github代码实现(希尔、冒泡、快排、堆排序、归并排序等)


本文算法代码的 github地址,希望大家看一下 写在最后部分,欢迎有想法的朋友沟通交流。

排序算法分类

​ 排序算法分为:插入类排序、交换类排序、选择类排序、归并类排序和基数类排序。

插入类排序

直接插入排序

  • 思路

    • 首元素有序,从[2, N]依次插入前面的有序表,并使其依然有序。
  • 稳定性:稳定的

  • 时间复杂度:

    • 平均:O(N的平方)
    • 最好:原始序列非递减有序(正序)
    • 最坏:原始序列非递增有序(逆序)
  • 代码实现

    • 代码实现如下:
    void InsertSort::Sort(ISortObj* objs[], int len)
    {
    	for (int i = 2; i <= len; ++i) {
    		for (int j = 1; j < i; ++j) {
    			if (objs[i]->LT(objs[j])) {
    				objs[0] = objs[i];
    				this->Move(objs, j, i - j, j + 1);
    				objs[j] = objs[0];
    			}
    		}
    	}
    }
    
    • 代码说明(下同)
      • 其中"ISortObj* objs[]"为待排序元素的指针数组,"int len"为待排序元素数据个数,默认下标0位置不存放元素数据,故objs数组的长度应为len+1。
      • 函数void Move(ISortObj* objs[], int start, int len, int pos):表示将objs数组的[start, start + len)数据移动到[pos, pos + len)位置,其具体实现在后文附录中给出。
      • 函数bool LT(ISortObj* pObj):表示本元素小于pObj指向的元素。

折半插入排序

  • 思路

    • 第一个元素有序,从[2, N]依次插入前面的有序表。然后用折半查找的方式定位第一个比带插入元素大的元素。
  • 稳定性:稳定的

  • 时间复杂度

    • 平均:O(N的平方)
  • 代码实现

    void BInsertSort::Sort(ISortObj* objs[], int len)
    {
    	for (int i = 2; i <= len; ++i) {
    		int high = i - 1;
    		int low = 1;
    
    		objs[0] = objs[i];
    		while (low <= high) {
    			int m = (high + low) / 2;
    			if (objs[0]->LT(objs[m])) {
    				high = m - 1;
    			}
    			else {
    				low = m + 1;
    			}
    		}
    		this->Move(objs, low, i - low, low + 1);
    		objs[low] = objs[0];
    	}
    }
    

希尔排序(缩小增量排序)

  • 思路

    • 每一趟希尔排序将元素序列分为d个子序列,分别对各子序列进行直接插入排序。
  • 稳定性:不稳定

  • 时间复杂度

    • 平均:O(N的1.X次方)
    • 最好:
    • 最坏:
  • 图解过程
    在这里插入图片描述

  • 代码实现

    void ShellSort::Sort(ISortObj* objs[], int len)
    {
    	for (int i = 0; i < this->_dlta.size(); ++i) {
    		ShellInsertSort(objs, len, this->_dlta[i]);
    	}
    }
    
    void ShellSort::ShellInsertSort(ISortObj* objs[], int len, int dk)
    {
    	for (int i = dk + 1; i <= len; ++i) {
    		objs[0] = objs[i];
    		if (objs[0]->LT(objs[i - dk])) {
    			int j = i - dk;
    			for (; j >= 1 && objs[0]->LT(objs[j]); j -= dk) {
    				objs[j + dk] = objs[j];
    			}
    			objs[j + dk] = objs[0];
    		}
    	}
    }
    

交换类排序

冒泡排序(改进)

  • 思路

    • 每趟排序,从首元素开始相邻两元素进行比较,如果逆序则交换。这样每一趟都会找出当前最大的元素沉到当前的末尾。最多进行n-1趟排序可以完成,如果中间某趟不进行交换也可以结束。
  • 稳定性:稳定的

  • 时间复杂度

    • 平均:O(N的平方)
    • 最好:原始序列非递减有序(正序)
    • 最坏:原始序列非递增有序(逆序)
  • 代码实现

    void BubbleSort::Sort(ISortObj* objs[], int len)
    {
    	for (int i = len; i > 1; --i) {
    		bool hasChange = false;
    		for (int j = 1; j < i; ++j) {
    			if (objs[j + 1]->LT(objs[j])) {
    				hasChange = true;
    				objs[0] = objs[j];
    				objs[j] = objs[j + 1];
    				objs[j + 1] = objs[0];
    			}
    		}
    		if (!hasChange) {
    			break;
    		}
    	}
    }
    

快速排序

  • 思路

    • 每一趟排序将当前序列分成两块,中轴左边的元素全部小于中轴,右边的全部大于等于中轴。
    • 其中每一趟方法如下:
      • 1、首先拿第一个元素作为中轴,建立两个下标,high指向最后一个元素,low指向第一个元素,只要high大于low就进行步骤2、3,最后将中轴元素放在low位置
      • 2、比较high与中轴:
        如果high小,则high元素放在low位置,low右移(low = low + 1),进行步骤3;
        否则,high左移(high = high - 1);
      • 3、比较low与中轴:
        如果low小,low右移(low = low + 1);
        否则,low元素放在high位置,high左移(high = high - 1),进行步骤2;
  • 稳定性:不稳定

  • 时间复杂度

    • 平均:O(nLogn)
    • 最好:原始序列非递减有序(正序)
    • 最坏:O(N的平方),正序或逆序时最坏时间复杂度
  • 图解过程
    在这里插入图片描述

  • 代码实现

    void QuickSort::QSort(ISortObj* objs[], int low, int high)
    {
    	if (low < high) {
    		int m = Partition(objs, low, high);
    		this->QSort(objs, low, m - 1);
    		this->QSort(objs, m + 1, high);
    	}
    }
    
    int QuickSort::Partition(ISortObj* objs[], int low, int high)
    {
    	int nLow = low;
    	int nHigh = high;
    	objs[0] = objs[nLow];
    	while (nLow < nHigh) {
    		while (nLow < nHigh && objs[0]->LT(objs[nHigh])) {
    			--nHigh;
    		}
    		if (nLow < nHigh) {
    			objs[nLow] = objs[nHigh];
    			++nLow;
    		}
    		while (nLow < nHigh && objs[nLow]->LT(objs[0])) {
    			++nLow;
    		}
    		if (nLow < nHigh) {
    			objs[nHigh] = objs[nLow];
    			--nHigh;
    		}
    	}
    	objs[nLow] = objs[0];
    	return nLow;
    }
    

选择类排序

堆排序

  • 堆概念介绍

    • 将堆看作一颗完全二叉树的顺序存储序列,小顶堆对应的完全二叉树中任意结点均比其孩子小或相等;大顶堆对应的完全二叉树中任意结点均比其孩子大或相等;不满足这两个性质的完全二叉树其顺序存储序列不是堆。
  • 思路

    • 将大顶堆的根节点与当前最后一个元素交换,之后将前面的元素序列重新调整为大顶堆,然后将新的大顶堆的根节点与当前最后一个元素(即整个序列倒数第二个)交换,重复此操作。
    • 调整大顶堆:比较堆顶与其两个孩子,若堆顶不是最大则将两孩子中的大者与堆顶“交换”,交换后的子树可能不再是堆,重复上述操作至最后。
    • 建立初始大顶堆:从最后一个非叶子结点开始向根结点方向逐步调整建堆。
    • 最坏时间复杂度O(nlogn),比快排好,且仅用一个记录辅助空间rc,但不稳定,
      建堆在正序时最慢,适合n大杂乱
  • 稳定性:不稳定

  • 时间复杂度

    • 平均:O(nlogn)
    • 最好:适合n大,且杂乱
    • 最坏:O(nlogn),建堆在正序时最慢
  • 图解过程

    • 构建大顶堆
      在这里插入图片描述
    • 调整大顶堆
      在这里插入图片描述
  • 代码实现

    void HeapSort::Sort(ISortObj* o[], int len)
    {
    	for (int i = len / 2; i > 0; --i) {
    		this->HeapAdjust(o, len, i);
    	}
    	for (int i = len; i > 1; --i) {
    		o[0] = o[i];
    		o[i] = o[1];
    		o[1] = o[0];
    		this->HeapAdjust(o, i - 1, 1);
    	}
    }
    
    void HeapSort::HeapAdjust(ISortObj* o[], int len, int root)
    {
    	o[0] = o[root];
    	for (int i = root * 2; i <= len; i *= 2) {
    		if (i < len && o[i]->LT(o[i + 1])) {
    			++i;
    		}
    		if (o[0]->LT(o[i])) {
    			o[root] = o[i];
    			root = i;
    		}
    		else {
    			break;
    		}
    	}
    	o[root] = o[0];
    }
    

归并类排序

归并排序

  • 思路

    • 若长为1则直接填入即完成排序;对长为n的序列,假设稍短的都能排序,先对子列R[s…(s+t)/2]和R[(s+t)/2+1…t]排序(递归),后调用归并函数进行归并。
  • 稳定性:稳定的

  • 时间复杂度:O(nlogn)

  • 空间复杂度:O(n)

  • 代码实现

    void MergeSort::Sort(ISortObj* objs[], int len)
    {
    	ISortObj** r = (ISortObj**)malloc(sizeof(ISortObj*) * (len + 1));
    	if (NULL == r) {
    		return;
    	}
    	MSort(r, objs, 1, len);
    	for (int i = 1; i <= len; ++i) {
    		objs[i] = r[i];
    	}
    }
    
    void MergeSort::MSort(ISortObj* r[], ISortObj* o[], int low, int high)
    {
    	if (low == high) {
    		r[low] = o[low];
    		return;
    	}
    	int m = (low + high) / 2;
    	this->MSort(r, o, low, m);
    	this->MSort(r, o, m + 1, high);
    
    	// 将r序列[]low, high]复制到o
    	for (int i = low; i <= high; ++i) {
    		o[i] = r[i];
    	}
    
    	Merge(r, o, low, m, high);
    }
    
    void MergeSort::Merge(ISortObj* r[], ISortObj* o[], int low, int m, int high)
    {
    	int i, j, k;
    	for (i = low, j = m + 1, k = low; i <= m && j <= high; ++k) {
    		if (o[i]->LT(o[j])) {
    			r[k] = o[i];
    			++i;
    		}
    		else {
    			r[k] = o[j];
    			++j;
    		}
    	}
    	while (i <= m) {
    		r[k++] = o[i++];
    	}
    	while (j <= high) {
    		r[k++] = o[j++];
    	}
    }
    

基数类排序

基数排序

  • 思路

    • 不进行比较,将原关键字看成由多个数位或字符构成的多关键字,按最低/高位优先进行分配、收集
    • 单关键字可能的取值个数称为基数rd,分配和收集的趟数d为最大原始关键字的位数
  • 稳定性:稳定

  • 复杂度:链式基数排序时间复杂度O(d*(n+ rd )),空间O(rd)

写在最后

总结

​ 时隔5年再次梳理这几个排序算法,也有了新的认识。

​ 在不考虑稳定性情况下,结合速度和空间两方面,个人觉得快速排序和堆排序是相对比较好的算法;归并排序适合数据量特别大,适合借助文件系统的排序场合;基数排序需要将关键字按位进行拆分,有些局限性。

​ 个人感觉所有平均时间复杂度为T(n*logn)的算法,都能从中找到二叉树的影子,或者说转化为二叉树来分析排序。尽量把每一次的查找和移位减小到一个二叉树的左子树或者右子树的范围内,而不是整棵树,这样较少位置查找的长度以及移位元素的个数和长度。快速排序是每一趟找一个中轴,中轴左右分大小两部分,然后中轴左右再进行内部的递归排序;堆排序本身就是借助二叉树;归并排序也是将待排序的序列分成左右两份,先递归排序每一份,再合并成一份。

​ 说的再直白一点,可能就是二叉树记录了之前每一趟排序元素之间比较的结果,之后的每一趟排序比较都可以利用前面每一趟的结果,从而减少本趟比较的次数。

猜想

​ 通过上面的分析,如果想一个新的算法,利用三叉树,会不会有更快的算法呢?时间复杂度会不会成为N*log3N呢?

​ 简单想了一下,应该没有哪个基于比较的排序算法的时间复杂度可以小于O(N*logN)了,因为每一次比较的结果只有两种,小于或者不小于,不会得出三种结果,所以每一次比较只能做到将序列分成两份,而不是分成三份,这就决定了算法的复杂度无法再减小了。

​ 注意上面说得是基于比较的算法最小的平均时间复杂度就是O(N*logN),而基数排序不是基于比较的,所以基数排序的平均时间复杂度看起来像是线性的。

附录

Move函数实现

void ISort::Move(ISortObj* objs[], int start, int len, int pos)
{
	if (pos == start) {
		return;
	}
	if (pos > start) {
		for (int i = 1; i <= len; ++i) {
			objs[pos + len - i] = objs[start + len - i];
		}
	}
	else {
		for (int i = 1; i <= len; ++i) {
			objs[pos + i] = objs[start + i];
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值