多种排序及其思想

基本排序

选择排序

这个排序顾名思义,就是一趟找出最大和最小,然后最小插入头,最大插入尾,头++,尾--,一个中规中矩的排序罢了,和我们之后说的排序完全没有可比性。

void XuanZeSort(int* arr,int n)
{
	int star = 0;
	int end = n - 1;
	while (star < end)
	{
		int min = star;
		int max = star;
		for (int i = star; i <= end; i++)
		{
			if (arr[i] < arr[min])
			{
				min = i;
			}
			if (arr[i] > arr[max])
			{
				max = i;
			}
		}
		swap(&arr[star], &arr[min]);
		if (star == max)
		{
			max = min;
		}
		swap(&arr[end], &arr[max]);
		star++;
		end--;

	}


}

冒泡排序

这也算一个经典排序,不过它和选择排序半斤八两,性能都很一般。

//冒泡排序
void BubbleSort(int* arr,int n)
{
	for (int i = 0; i < n; i++)
	{
		int tag = 0;
		for (int j = 1; j < n - j; j++)
		{
			if (arr[j - 1] > arr[j])
			{
				swap(&arr[j - 1], &arr[j]);
				tag++;
			}
		}
		if (tag == 0)
		{
			break;
		}
	}
}

插入排序

这个就不那么一般了,可以说它还不错的,只是比不上那些更好的排序。

//插入排序
void insert(int* arr, int k)
{
	for (int i = 0; i < k-1; i++)
	{
		int end = i;
		int tmp = arr[end + 1];
		while (end >= 0)
		{
			//每次用tmp和end比较 如果end小 那么就交换 然后end-- 继续对比 直到找到合适的位置 
			if (tmp < arr[end])
			{
				arr[end + 1] = arr[end];
				--end;
			}
			//若tmp不是最小 那么就不需要继续对比下去 直接退出 进行赋值即可
			else
			{
				break;
			}
		}
		//由于while每次会--end 所以最后end处于-1的位置 需要+1才能对正,
        另一种情况则是提前找到了break出来,那我们只要在它后面插入即可,
        所以也+1
		arr[end + 1] = tmp;
	}

}

可以看出来,插入和上面的两位还是有点区别的,tmp巧妙地在成为参数的同时记录了原本的数据,使得我们不需要在创建一个新变量存储数据,只需要在tmp到达合适位置时把它放在end+1的位置即可。

大哥排序

希尔排序

//希尔排序
void Shell(int* arr,int k)
{
	int gap = k;
	while (gap > 1)
	{
		//gap具体是几其实没有定论 /3可以/4也可以 除的越大 预处理的范围越大,但也相对越无序,
		//除的越小 范围越小 需要的次数也越多 但也越有序 为1时则是经典的插入排序
		gap /= 3 + 1;
		//-gap是因为tmp会有一个+gap的行为,所以如果设置成i<k则会发生越界的问题
		for (int i = 0; i < k - gap; i++)
		{
			//将这个数据分为每组有gap个数据的小组 ,再对比这些间隔了gap个数据的大小 看是否需要进行交换
			//大体逻辑其实和插入排序一样 不过是添加了一个根据gap对数据先进行预排序 以期望达到提高效率的过程
			int end = i;
			int tmp = arr[end + gap];
			while (end >= 0)
			{
				if (tmp < arr[end])
				{
					arr[end + gap] = arr[end];
					end -= gap;
				}
				else
				{
					break;
				}
			}
			arr[end + gap] = arr[end];

		}
	}
	
}

其实这就是插入排序的加强版,它巧妙的通过将数据分组,再把每组的数据进行粗略的预排序,然后再进行一次插入排序,所以这也可以算是加了优化的插入排序,不过它却实挺能打的。

堆排序

//堆排序

void HeapDown(int* pst, int size, int tmp)
{
	//tmp是我们传入的父节点 那么*2+1则可得到子节点
	int child = tmp * 2 + 1;

	//只要孩子还小于我们的end 就继续循环
	while (child < size)
	{
		//在进行比较交换前 先确定我们的 右孩子存在 且左孩子大于右孩子
		//这样我们就++孩子 用大的那个
		if (child + 1 < size && pst[child + 1] > pst[child])
		{
			child++;
		}
		//经过上面的if 现在孩子是大的那个 
		if (pst[child] > pst[tmp])
		{
			//交换父子位置
			swap(&pst[tmp], &pst[child]);
			//重新定义子位置和父位置 然后继续比较 
			//最后孩子会到它应该在的位置 
			tmp = child;
			child = tmp * 2 + 1;

		}
		else
		{
			break;
		}
	}

}

void big(int* arr, int n)
{
	for (int i = 1; i < n; i++)
	{
		//首先先建立一个大堆(或者小堆 看需求)
		HeapDown(arr, i, 0);
	}
	//记录最后一个位置的下标
	int end = n - 1;//例 k = 10 则end = 9
	while (end > 0)//end决定我们走几次循环
	{
		//由于上面已经建立过堆了 所以首位一定是最大的,而末位一定是最小的 先交换它们
		swap(&arr[0], &arr[end]);
		//完成一趟 --end 然后看条件还满不满足 满足则继续排 最后有序
		HeapDown(arr, end, 0);
		--end;
	}
}

这就利用了二叉树的大小堆结构来排序,但是由于大小堆不管孩子之间的关系,所以我们手动来创造条件,筛选出两个孩子中满足的进行交换,最后得到一个有序的数组。

快速排序

//三种快排
//三数取中
int FindMid(int* arr,int star,int end)
{
	//头尾都好确定 唯独中间值 不好找 所以我们取 头尾中 这三个数中比较中位的数当中间值
	int mid = (star + end) / 2;
	if (arr[star] < arr[mid])
	{
		if (arr[mid] < arr[end])
		{
			return mid;
		}
		else if (arr[star] < arr[end])
		{
			return end;
		}
		else
		{
			return star;
		}

	}
	else
	{
		if (arr[mid] > arr[star])
		{
			return mid;
		}
		else if (arr[star] < arr[end])
		{
			return star;
		}
		else
		{
			return end;
		}
	}
}
//霍尔快排
int sort(int* arr,int star,int end)
{
	int open = star;
	while (star < end)
	{
		//由于每次外层while进来都有可能改变end或star 所以外层的判断条件 里面还得判断一次
		while (star < end && arr[end] >= arr[open])
		{
			--end;
		}
		while (star < end && arr[star] <= arr[open])
		{
			++star;
		}
		//尾找小 头找大 找到后交换它们的位置 
		swap(&arr[star], &arr[end]);
	}
	//出来后 再交换open 和 star 的位置即可返回  此时open才是star的初始位置 而star++多次后已经和end相遇
	swap(&arr[open], &arr[star]);
	return star;
}
//挖坑快排
int sort2(int* arr, int star, int end)
{
	
	int open = arr[star];
	//记录坑的下标
	int K = star;
	while (star < end)
	{
		//同霍尔方法一样 各找各的
		while (star < end && arr[end] >= arr[open])
		{
			--end;
		}
		//此时end一定是小于坑的 所以 把它填坑里去 end成为新的坑
		arr[K] = arr[end];
		K = end;
		while (star < end && arr[star] <= arr[open])
		{
			++star;
		}
		//此时star必定是大于新坑的 所以把它填进去 star变新坑 
		arr[K] = arr[star];
		K = star;
	}
	//经过上面的多次变坑 最后star和end相遇停下 最后记录一下坑的位置然后返回 下次就从这继续 多次过后便有序了
	arr[K] = open;
	return K;
}
//前后指针快排
int sort3(int* arr, int star, int end)
{
	//定义前后指针和参照物
	int prev = star;
	int next = star + 1;
	int key = star;

	while (next <= end)
	{
		//如果快指针小于参照物 那么交换它们 不过由于前后指针可能相遇 为了不进行不必要的交换 所以添加一个判断
		//即慢指针走一步 如果不等于快指针 那才交换 否则的话 交不交换都一样 所以直接++快指针即可
		if (arr[next] < arr[key] && ++prev != next)
		{
			swap(&arr[prev], &arr[next]);
		}
		++next;
	}
	//交换 返回
	swap(&arr[star], &arr[prev]);
	
	return prev;
}

void Quicksort(int* arr,int star,int end)
{
	//三数取中优化版本
	if (star>=end)
	{
		return;
	}
	//如果数据少于x个 那我们直接用别的排序快速给他排好返回即可 不必拆到底
	if ((end - star + 1) < 10)
	{
		insert(arr + star, end - star + 1);

	}
	//经过三数取中筛选 每次都是最好的情况 效率大大提升
	else
	{
		int mid = FindMid(arr, star, end);
		swap(&arr[star], &arr[mid]);
		int open = star;
		int begin = star; int over = end;
		while (begin < over)
		{
			while (begin < over && arr[over] >= arr[open])
			{
				--over;
			}
			while (begin < over && arr[begin] <= arr[open])
			{
				++begin;
			}
			swap(&arr[begin], &arr[over]);
		}
		swap(&arr[open], &arr[begin]);
		open = begin;
		Quicksort(arr, star, open - 1);
		Quicksort(arr, open + 1, end);
	}
	
    //sort1-3都可以用二叉树前序来递归
	/*int insert = sort(arr, star, end);
	Quicksort(arr, star, insert - 1);
	Quicksort(arr, insert + 1, end);*/
}

快排就有很多版本了,只能说都是大哥们写的优化,没有明显的强弱之分,看个人习惯选用即可,下面还有非递归版本的。

//非递归快速排序
#include "stack.h"//利用栈模拟实现递归的行为 故引入栈的文件
void NoDGQuickSort(int* arr,int star,int end)
{
	//初始化栈 
	SL stack;
	SLInit(&stack);
	SLPush(&stack, end);
	SLPush(&stack, star);
	
	while (!SLempty(&stack))
	{
		//取出刚才存入的左右区间
		int left = SLTop(&stack);
		SLPOP(&stack);
		int right = SLTop(&stack);
		SLPOP(&stack);
		//根据左右区间进行一次排序
		int keyi = sort3(arr, left, right);
		//再分出左右区间 下一次while继续排 模拟实现递归的步骤 最终有序
		if (keyi + 1 < right)
		{
			SLPush(&stack, right);
			SLPush(&stack, keyi + 1);
		}
		if (keyi - 1 > left)
		{
			SLPush(&stack, keyi-1);
			SLPush(&stack, left);
		}
	}
	SLDestroy(&stack);
}

我只能说在排序里叫快排的肯定有两把刷子,至少我在了解这些写法的时候总会忍不住喊个妙出来。

归并排序

//递归归并排序
void _MergeSort(int* arr,int star,int end,int* tmp)
{
	//如果头尾相等 代表这个区间只有一个数 就不用排序了 直接返回上一层递归
	if (star == end)
	{
		return;
	}

	//众所周知二叉树最后一层的数据占了整体的一半 所以递归到后面效率会变得很低 所以写了一个优化 让它不会递归到最后
	//如此 在数据较少的情况下不进行二分 而是直接使用插入排序(当然其他也行 看个人) 进而提升效率
	if (end - star + 1 < 10)
	{
		insert(arr + star, end - star + 1);
		return;
	}
	//进来先划分区间 
	int mid = (star + end) / 2;
	//以二叉树后序的方式进行递归
	_MergeSort(arr, star, mid, tmp);
	_MergeSort(arr, mid+1, end, tmp);

	//若左右区间皆已返回 则代表上一层数据有序 此时再对比左右区间的数尾插
	int star1 = star; int end1 = mid;
	int star2 = mid + 1; int end2 = end;

	int i = star;
	while (star1 <= end1 && star2 <= end2)
	{
		if (arr[star1] < arr[star2])
		{
			tmp[i++] = arr[star1++];
		}
		else
		{
			tmp[i++] = arr[star2++];
		}
	}
	//插到最后一定会剩一个最大的没插入 此时两个while只会进一个 又 因为有序 所以只要尾插就行
	while (star1 <= end1)
	{
		tmp[i++] = arr[star1++];
	}
	while (star2 <= end2)
	{
		tmp[i++] = arr[star2++];
	}
	//最后将tmp有序的数组存到原本的arr上 +star是由于每次循环左右区间并不都是从头开始 所以需要加上偏移量
	memcpy(arr + star, tmp + star, sizeof(int) * (end - star + 1));
}
void MergeSort(int* arr,int n)
{
	//开一个保存数据的数组空间
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		assert(tmp);
		return;
	}
	//用附属函数进行递归调用
	_MergeSort(arr, 0, n - 1, tmp);
	free(tmp);
	tmp = NULL;
}


//非递归归并排序
void MergeSort(int* arr, int n)
{
	//同上 开空间
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		assert(tmp);
		return;
	}


	//根据递归方法的思想 其实就是一直在二分 所以一进来 我们直接从最底层开始比较 也就是和递归方法反着来  故 gap设为1
	int gap = 1;
	while (gap < n)
	{
		//这个for循环其实就是在模拟每次二分的过程 
		for (int i = 0; i < n; i += 2 * gap)
		{
			//将数据带入区间
			int star1 = i; int end1 = i + gap - 1;
			int star2 = i + gap; int end2 = i + 2 * gap - 1;

			//由于上面参数的设计 会出现越界的问题 为了不影响数据 所以做个保险 当star2都越界的情况下 end2必然也是越界的 所以不需要排这个区间的数据 直接退出即可
			if (star2 >= n)
			{
				break;
			}
			//当end2越界 又因为上面if的判断 所以此区间只有一个end2越界 那我们只需要修改end2的边界即可 
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			// 逻辑一样 故Ctrl+C Ctrl+V
			int j = 1;
			while (star1 <= end1 && star2 <= end2)
			{
				if (arr[star1] < arr[star2])
				{
					tmp[j++] = arr[star1++];
				}
				else
				{
					tmp[j++] = arr[star2++];
				}
			}
			while (star1 <= end1)
			{
				tmp[j++] = arr[star1++];
			}
			while (star2 <= end2)
			{
				tmp[j++] = arr[star2++];
			}
			//需要和上面区分开来的是由于我们这种方式会越界 所以有时候有直接break的可能 所以每一次 都需要根据当时情况进行拷贝
			//如果和上面一样最后进行一次cpy 那么数据会有多或者少的问题出现 所以在每一次循环末尾进行一次拷贝
			memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		//最后 gap *= 2 模拟当层递归完成返回上一层的变化
		gap *= 2;
	}
	//销毁空间
	free(tmp);
	tmp = NULL;
}
int main()
{

	//Shell
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int k = sizeof(arr) / sizeof(arr[0]);
	insert(arr, k);
	return 0;
}

  • 10
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值