八大排序

1. 前言

        本文主要是记录常用的排序算法

       

2. 各排序算法时间复杂度

            

       注:排序的稳定性是指如果在排序的序列中,存在前后相同的两个元素的话,排序前 和排序后他们的相对位置不发生变化

3.交换排序

3.1快速排序

快速排序的思想可以由以下描述:

接下来只要对下标0~i-1和下标 i+1~n-1做同样的操作即可,代码如下

void swap(int *a, int *b){
	int temp = *a;
	*a = *b;
	*b = temp;
}

/*输入为:整型数组;素组有效开始索引;有效结束索引
*/
void Qsotr(int *arr, int idxI, int idxJ)
{
	if (arr == NULL || idxJ < 1) return;

	int key = arr[idxI];
	int i = idxI;
	int j = idxJ;

	while (i != j){

		do{
			if (arr[j]< key){
				swap(arr[i], arr[j]);
				break;
			}
			else{
				--j;
			}
		}while(i != j);

		while (i != j){
			if (arr[i]> key){
				swap(arr[i], arr[j]);
				break;
			}
			else{
				++i;
			}
		} 
	}

	if (i - 1 > 0){
		Qsotr(arr, 0, i - 1);
	}
	if (j + 1 < idxJ){
		Qsotr(arr, j + 1, idxJ);
	}

}

     快排的时间复杂度是O(n*log2n),如果数组有序,则快排退化至冒泡排序,时间复杂度为O(n^2),一个改进是对数组最左、最右以及中间这3个数作比较,取中间值放在最右边作为key值。

3.2 冒泡排序

     冒泡排序通过两两比较的方式,一次循环比较即可把最大的值或最小的值放至最右边

/*
冒泡排序即可以通过两两比较将小的数从下标0到 len-1上升
也可以把大的数从下标0到 len-1 上升
*/
void swap(int *a, int *b)
{
	int temp = *a;
	*a = *b;
	*b = temp;
}

void BubbleSort(int *arr, int len)
{
	if (arr == NULL||len<2) return;

	for (int i = len - 1; i > 0; --i){
		for (int j = 0; j < i; ++j){   // 一次for 循环可把最大数放至最右端,
			//故最右边不用比较
			if (arr[j] > arr[j + 1]){         //大的数传到右边
				swap(arr[j],arr[j + 1]);
			}
		}
	}
}
 /*改进: * 每一轮循环开始前设置标志位,如果本轮循环没有交换任何元素, *则说明所有元素已经有序,可以提前结束排序 */
void BubbleSortImpro(int *arr, int len)
{
	bool flag = true;
	for (int i=len-1;i>0; --i){
		for (int j = 0;j<i; ++j){   // 一次for 循环可把最大数放至最右端,
			//故最右边不用比较
			if (arr[j]>arr[j + 1]){         //大的数传到右边
				swap(arr[j], arr[j + 1]);
				flag = false;
			}
		}

		if (flag) break;
	}
}
4.插入排序
4.1 直接插入排序
      直接插入排序原理可由这张表表示

      

void DirectInsertSort(int *arr, int len)
{
	for (int i = 1; i < len; ++i){

		if (arr[i] < arr[i - 1]){   //只有第i个数小于第i-1个数,才可能将i插入于 i-1 之前  
			int move = arr[i];
			int j = i;

			while (move<arr[j - 1] && j>0){
				arr[j] = arr[j - 1];
				--j;
			}
			if (i != j){
				arr[j] = move;
			}

		}

	}
}
4.2 希尔排序

      下图可以形象地看出希尔排序的思想,即以一定量的增量选择将数组分隔为多组,之后作直接插入排序,一般的初次取序列的一半为增量,以后每次减半,直到增量为1。

      

void oneSort(int *arr, int len, int d)    //一趟排序
{
	for (int i = 0; i < d; ++i){
		int k = i + d;
		while (k <  len){
			int j = k - d;
			int key = arr[k];
			while (key  <  arr[j] && j >= i){
				arr[j + d] = arr[j];
				j = j - d;
			}
			arr[j + d] = key;

			k += d;
		}
	}

}

void ShellSort(int *arr, int len)
{
	int d = len;
	while (d != 1)
	{
		d = d / 2;    //每次取一半,增量选择为[5, 2 , 1]
		oneSort(arr, len, d);
	}

}
      希尔排序算法在最坏的情况下和平均情况下执行效率相差不是很多,下界是O(n*log2n),与增量序列的选取有关,基本上会比o(n^2)好一些,希尔排序是非稳定排序算法


5 基数排序/ 木桶排序

基数排序又称为木桶排序,基数排序适合整数或者特定格式的浮点数,以下为木桶排序的思想



    自己写的代码,for循环有点多,效率有些低

int getBs(int *arr, int len)
{
	int maxVal = arr[0];
	for (int i = 1; i < len; ++i){
		maxVal = arr[i] > maxVal ? arr[i] : maxVal;
	}

	int count = 1;

	while (maxVal / 10 != 0){
		count++;
		maxVal = maxVal / 10;

	}

	return count;
}

//返回整数 val 第 idxBit 位的数字
int getBitsIdx(int x, int maxBsNum, int idxBit)
{
	if (idxBit == 1){   //各位数
		return x % 10;
	}
	else if (idxBit == maxBsNum){
		return x / int(std::pow(10, maxBsNum - 1));
	}
	else{
		return (x / int(std::pow(10, maxBsNum - 1))) % 10;

	}

}

void RdixSort(int *a, int len)
{
	if (len <= 1)  return;

	int maxBsNum = getBs(a, len);

	int *temp = new int[len];

	for (int i = 1; i <= maxBsNum; ++i){
		int m = 0;
		for (int j = 0; j < 10; ++j){
			for (int k = 0; k < len; ++k){
				if (getBitsIdx(a[k], maxBsNum, i) == j){
					temp[m++] = a[k];
				}
			}
		}

		memcpy(a, temp, sizeof(int)*len);
	}

	delete temp;
}
可以看看百度百科上的,比较巧妙的统计0~9这10只桶子的频率,之后转换为temp中的各只桶子所含数的位置,逆序将各只桶子的数存入temp
int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
	int d = 1; //保存最大的位数
	int p = 10;
	for (int i = 0; i < n; ++i)
	{
		while (data[i] >= p)
		{
			p *= 10;
			++d;
		}
	}
	return d;
}
void RdixSort(int data[], int n) //基数排序
{
	int d = maxbit(data, n);
	int *tmp = new int[n];
	int *count = new int[10]; //计数器
	int i, j, k;
	int radix = 1;
	for (i = 1; i <= d; i++) //进行d次排序
	{
		for (j = 0; j < 10; j++)
			count[j] = 0; //每次分配前清空计数器
		for (j = 0; j < n; j++)
		{
			k = (data[j] / radix) % 10; //统计每个桶中的记录数
			count[k]++;
		}
		for (j = 1; j < 10; j++)
			count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
		for (j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
		{
			k = (data[j] / radix) % 10;
			tmp[count[k] - 1] = data[j];
			count[k]--;
		}
		for (j = 0; j < n; j++) //将临时数组的内容复制到data中
			data[j] = tmp[j];
		radix = radix * 10;
	}
	delete[]tmp;
	delete[]count;
}
6选择排序

6.1 简单选择排序

    简单选择排序思想很简单,它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

void SelectSort(int *arr, int len)
{
	for (int i = 0; i < len-1; ++i){    //最后一个的时候就不用找了

		int minVal = arr[i];           // 取最小(最大也可以 )
		int idx = i;
		for (int j = i + 1; j<len; ++j){
			if (arr[j] < minVal){
				minVal = arr[j];
				idx = j;
			}
		}
		if (arr[i]!= minVal){
			arr[idx] = arr[i];
			arr[i] = minVal;
		}
	}

}
6.2 堆排序

     堆是一种数据结构,首先其是一种完全二叉树。
     完全二叉树指的是在最后一层上只缺少右边的若干结点,其余各层节点数达到最大的一种树形结构,如下所示:

      

      令完全二叉树的一个节点为i ( i < n ),当二叉树满足以下公式之一时,其为堆,共有2种堆,满足公式1,则为大跟堆,满足公式2,则为小跟堆

      

     在堆排序中定义一种下沉调整节点的操作,与大跟堆为例,即某节点会不断与其子节点中较大的一个作交换。对序列中节点0,1,2,..., [n/2]-1,作下沉调整即可将完全二叉树调整为大跟堆,其次将节点0与节点n-1作交换,这时最小的值已经被保存到序列末尾了,这时就要对前n-1个节点中的第0个节点作下沉,又可以调整为堆,之后不断重复直到n个值为有序。代码如下:

// 将nodeIdx处的节点不断与字节点中较大的一个作交换,即相当于往较大的子节点不断"下沉"
void adjustNode(int* arr, int nodeIdx, int adjustlen)
{
	int  idx = nodeIdx;
	int subIdx = 2 * nodeIdx + 1;   //nodeIdx的左子节点

	while (subIdx < adjustlen){
		//右子节点存在且右子节点大于左子节点
		if (subIdx + 1< adjustlen &&    arr[subIdx + 1]  > arr[subIdx]){
			subIdx++;    //subIdx为右子节点位置
		}
		// 子节点较大的一个与节点作比较,如果大于则交换
		if (arr[subIdx] >  arr[idx]){
			int temp = arr[subIdx];
			arr[subIdx] = arr[idx];
			arr[idx] = temp;
		}
		//传递到下一层
		idx = subIdx;
		subIdx = 2 * idx + 1;
	}

}

void HeapSort(int* arr, int len)
{
	//首先要把数组调整成堆,这里以大顶堆的形式
	//只需对前半数节点作调整即可实现大跟堆
	for (int i = (len / 2 - 1); i >= 0; --i){
		adjustNode(arr, i, len);
	}
	//迭代找出0~i 范围的最大值,保存至末尾
	for (int i = len-1; i >0; --i){
		int temp = arr[i];
		arr[i] = arr[0];
		arr[0] = temp;
		adjustNode(arr, 0, i);         //将第0个节点下沉下来
	}

}
7 归并排序
      归并排序是采用分治法实现排序的,下图即为归并排序思想
   

先实现个非递归版本吧
     以下为百度百科上归并排序的思路,大括号看成一个batch,一个batch中有两个集合。

     

void Merge2(int *arr, int  a, int b, int length, int n)
{
	int right;
	//判断第2个集合的结束指针下标是否大于n-1了,
	//相当于判断第2个集合是否完整
	if (b + length - 1  > n - 1)  right = n - b;
	else{
		right = length;
	}
	int * temp = new int[length + right];
	int i = 0, j = 0;
	while (i <= length - 1 && j <= right - 1){
		if (arr[a + i] <= arr[b + j]){
			temp[i + j] = arr[a + i];
			++i;
		}
		else{
			temp[i + j] = arr[b + j];
			++j;
		}
	}
	if (j == right){
		memcpy(temp + i + j, arr + a + i, (length - i) *  sizeof(int));
	}
	else if (i == length){
		memcpy(temp + i + j, arr + b + j, (right - j) *  sizeof(int));
	}
	memcpy(arr + a, temp, (right + length) *sizeof(int));   //在a的地址插入
	delete[] temp;    //数组的话要[]

}

void MergeSort(int *arr, int len)
{
	int step = 1;
	//步长从1到2、4、8 ......不断增加
	// 以2×step作为1个batch的长度,step则可以看成是batch里两个集合的完整长度
	//排序思路如下:
	//首先一个batch通常含有2个集合,batch以2,4,8,....  的形式不断向前跳跃
	//
	//最外层的step必须小于整个序列长度,如果大于等于的话,说明就算以0开始,第一个集合的结束下标就大于
	//等于arrLen,则这个序列不存在2个集合了
	while (step <len){
		// i+step实际就为batch里后个集合的开始指针,这个开始指针至少应该小于等于序列最后下标,这样说明
		// 这个batch里才能有两个集合
		for (int i = 0; i + step <= len-1; i += 2 * step){  //batch大小是2*step
			// 下标i为第一个集合的起点指针,i+step为第2个集合的起点指针
			//指针包含在内的集合长度为step,该函数首先要调整好第2个集合的长度
			//一开始默认为step,当第2个集合的最后一个元素,其位置可表示为 b+step-1,
			//如果这个位置小于等于arrLen-1,说明当前这个集合完整,如果大于,说明集合里有元素
			//是缺的,把集合长度由step改成(n-b)
			Merge2(arr, i, i + step, step, len);
		}

		step *= 2;
	}

}
       再来个递归版本,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。
      未完待续......


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值