一些排序算法

一.插入排序

插入排序的主要思想类似于摸扑克牌,在已经有序的一个区间内插入新的值,找到合适的位置插入,然后再将原来位置的值后移。

//插入排序
//时间复杂度O(N^2)
//最坏情况:逆序,987654321,需要移动的次数为1,2,3,4,....n-1,等差数列
//最好情况,顺序,O(N)
void InsertSort(int* a, int n)
{
	// [0,end]有序,把end+1位置的值插进去,让[0,end+1]有序
	for (int i = 0; i < n - 1; i++) //n-1,防止越界
	{
		int end = i; //从0开始插入,类似摸扑克牌
		int tmp = a[end + 1];
		while (end >= 0)
		{
			if (a[end] > tmp)
			{
				a[end + 1] = a[end];
				end--;
			}
			else
			{
				break;
			}

		}
		a[end + 1] = tmp;//无论是循环结束还是中途结束,都是break,然后把数放在end的后一个
	}
	
}

二.希尔排序

希尔排序可以看作插入排序的一种升级,当要排序的数非常多时,我们可以设置一个gap,例如gap为5,则5个数据被分为一组,然后间隔5个空格对数据进行排序,然后不断缩小gap进行排序,当gap最后为1时,其实就是插入排序。

//希尔排序
//1.先进行预排序,让数组接近有序 2.直接插入排序
//多组间隔为gap的预排序,gap由大变小,gap越大,大的数越快的到后面,小的数越快到前面
//gap越大,预排序完越不接近有序,gap越小,越接近有序,当gap==1时,就是插入排序
//时间复杂度:O(logN*N)
void ShellSort(int* a, int n)
{
	int gap = n;

	while (gap > 1)  //2^x = N ,时间复杂度为logN(2为底)
	{
		gap = gap / 2; //保证最后一次为1,即插入排序,gap>1都是预排序
		//gap = gap / 3 + 1;
		//这里每次i只会增加一个,也就是end每次向后增加一个,这样可以把间隔为gap的多组数据同时排
		//gap很大时,下面预排序时间复杂度O(N),  gap很小时,预排序已经完成,很接近有序,还是O(N)
		for (int i = 0; i < n - gap; i++)
		{
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (a[end] > tmp)
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
				{
					break;
				}

			}
			a[end + gap] = tmp;
		}
	
	}
	
}

三.直接选择排序

 选择排序的思路很简单,从数组两头开始找到一个最大的和一个最小的数,分别放在两端,然后更新区间继续寻找最大的数和最小的数,注意一下代码中的细节

//直接选择排序 O(N^2) N+N-1+N-2+......
void SelectSort(int* a, int n)
{
	int begin = 0;
	int end = n - 1;
	while (begin < end)  //更新区间
	{
		int mini = begin, maxi = begin;
		for (int i = begin; i <= end; i++) //在区间内找到一个最大的和最小的
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}

		Swap(&a[begin], &a[mini]);
		if (begin == maxi)  //防止begin==maxi时,最大值被换走,更新一下下标
		{
			maxi = mini;
		}
		Swap(&a[maxi], &a[end]);
		++begin;
		--end;
	}
}

四.堆排序

1.堆的物理结构和逻辑结构

堆的物理结构是一个数组,逻辑结构是一颗完全二叉树

其中大堆表示父节点的值比孩子节点的值大,小堆是父节点的值比孩子节点的值小,堆排序就是借助堆帮助选择,因为堆顶的元素是最大或者最小的。

2.建堆和向下调整算法
 

1.首先把数组想象成一个完全二叉树,然后进行建堆

2.向下调整算法。算法前提:左右子树都是大堆或者小堆

过程:从根节点开始,选出左右孩子较小或较大的那个和父亲比较,满足条件则交换,然后继续往下,调到叶子节点终止。正是因为左右子树均是大堆或者小堆,才能完成这个交换

void AdjustDown(int* a,int n,int root)  //向下调整算法,必须满足左右子树是大堆or小堆,这里是大堆
{
	int parent = root;
	int child = parent * 2 + 1; //默认是左孩子
	while (child < n)
	{
		//1.选出左右孩子中大的那个
		if (a[child + 1] > a[child] && child + 1 < n) //防止越界
		{
			child += 1;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

但是对于一个一般的数组,左右子树并不满足大堆或者小堆,这里不能直接使用向下调整算法。 要自下而上,从最后一颗子树开始调整,因为叶子节点只有一个,不需要调,从最后一个非叶子节点开始调。

 

 在这里我们先对8这个数进行调整,再对7,然后是2,接着是5,这样自下而上调整,当我们要调整顶上时,下面已经是调整好的大堆或者小堆,还是利用节点之间的父子关系找到8。

//建堆 时间复杂度O(N)
for (int i = (n - 1 - 1) / 2; i >= 0; --i) //倒数的第一个非叶子节点开始调整 ,child=(parent-1)/2
{
	AdjustDown(a, n, i);
}

接着我们需要考虑排升序应该是建大堆还是小堆,在这里我们应该建大堆,每一次将最大的换到最后,然后不把他看作堆里的值,此时左右仍满足大堆,前n-1个数向下调整找出次大的值。

这里图二是建小堆,如果这样做的话,虽然可以找到最小的数,但是找次小的数时,需要拿第二个数做根,此时的父子关系乱了,左右也不满足小堆,又需要重新建堆,效率太低,不如直接遍历

void HeapSort(int* a,int n)  //整体时间复杂度:O(N*logN)
{
	//建堆 时间复杂度O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i) //倒数的第一个非叶子节点开始调整 ,child=(parent-1)/2
	{
		AdjustDown(a, n, i);
	}

	//排升序,建大堆,如果这里建小堆,可以找到最小的数,
	//但是找次小的数的时候需要拿第二个数去做根,剩下的树的父子关系全乱了,只能重新建堆,效率太低,不如直接遍历
	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);  //最大的换到最后,不把它看做堆里面的,此时左右子树都满足大堆,向下调整找出次大的
		AdjustDown(a, end, 0);
		end--;
	}


}

到此堆排序完成 

五.快速排序

快速排序的基本思想:

任取待排序
元素序列中的某元素作为基准值,按照该基准将待排序集合分割成两子序列,左子序列中所有
元素均小于基准值,右子序列中所有元素均大于基准值,然后对左右子序列重复该过程

 1.挖坑法

//挖坑法
int PartSort1(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);
	int begin = left, end = right;
	int pivot = begin;
	int key = a[begin];
	while (begin < end)
	{
		//右边找小,放到左边
		while (begin < end && a[end] >= key)
		{
			--end;
		}
		//小的放到左边的坑里,同时自己形成坑位
		a[pivot] = a[end];
		pivot = end;

		//左边找大,放在右边
		while (begin < end && a[begin] <= key)
		{
			++begin;
		}
		//大的放到右边的坑里,自己形成新的坑
		a[pivot] = a[begin];
		pivot = begin;
	}
	pivot = begin;
	a[pivot] = key;

	return pivot;
}

 2.左右指针法,与挖坑法类似


//左右指针法
int PartSort2(int* a,int left,int right)  
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);
	
	int begin = left, end = right;
	int keyi = begin;
	
	while (begin < end)
	{
		//找小
		while (begin < end && a[end] >= a[keyi])
		{
			--end;
		}
		//找大
		while (begin < end && a[begin] <= a[keyi])
		{
			++begin;
		}

		Swap(&a[begin], &a[end]);

	}

	Swap(&a[begin], &a[keyi]);
	
	return begin;

}

 3.前后指针法

cur找小,每次遇到比key小的值就停下来,++prev,然后交换

当cur和prev距离变大时,说明中间有很多比key大的数,如果这个时候再找到一个比key小的,++prev就指向了大的那个数,然后将大的换到右边小的换到左边。最后再把prev和key交换,就满足快速排序的基本思想

//前后指针法
int PartSort3(int* a, int left, int right)
{
	int index = GetMidIndex(a, left, right);
	Swap(&a[left], &a[index]);

	int keyi = left;
	int prev = left, cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < a[keyi])
		{
			++prev;
			Swap(&a[prev], &a[cur]);
		}

		++cur;
	}
	Swap(&a[keyi], &a[prev]);
	return prev;
}

4.快速排序的改进

1.三数取中。快速排序对基准的选择很重要,如果序列是有序的,快排的效率会很低,用三数取中改进这个问题

//三数取中  解决快排在有序情况下效率低的问题
int GetMidIndex(int* a, int left, int right)
{
	int mid = (left + right) >> 1;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] < a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
	else   //a[left] > a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

2.小区间优化

//快速排序
void QuickSort(int* a, int left, int right)
{
	if (left >= right)
	{
		return;
	}

	//int keyIndex = PartSort1(a, left, right);
	// int keyIndex = PartSort2(a, left, right);
	int keyIndex = PartSort3(a, left, right);

	
	//QuickSort(a, left, keyIndex - 1);
	//QuickSort(a, keyIndex + 1, right);

	//小区间优化
	if (keyIndex - 1 - left > 10)
	{
		QuickSort(a, left, keyIndex - 1);
	}
	else
	{
		InsertSort(a + left, keyIndex - 1 - left + 1);
	}
	if (right - (keyIndex + 1) > 10)
	{
		QuickSort(a, keyIndex + 1, right);
	}
	else
	{
		InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
	}
}

 5.快速排序的非递归

前面我们实现的快速排序都是使用递归,但是递归的缺陷是可能会造成栈溢出。这里我们利用数据结构中的栈来完成快速排序的非递归。利用栈存放需要排序的数组。当完成第一趟快排后,将数组划分成两段存到栈里,因为栈是后进先出,所以如果想要先处理左边的,就要先把右边的数组压栈。然后再对左区间出栈,进行第二趟快速排序,等到栈空时,排序就完成了。

//快速排序非递归  利用数据结构栈模拟过程
void QuickSortNonR(int* a, int n)
{
	ST st;
	StackInit(&st);
	StackPush(&st, n - 1);
	StackPush(&st, 0);

	while (!StackEmpty(&st))
	{
		int left = StackTop(&st);
		StackPop(&st);

		int right = StackTop(&st);
		StackPop(&st);

		int keyIndex = PartSort1(a, left, right); //单趟排序
		if (keyIndex + 1 < right)
		{
			StackPush(&st, right);
			StackPush(&st, keyIndex + 1);
		}
		if (left < keyIndex - 1)
		{
			StackPush(&st,keyIndex - 1);
			StackPush(&st,left);
		}
	}
	StackDestory(&st);
}

六.归并排序

归并排序是一种分治思想,当我想要整个序列有序,可以先让左边和右边有序,但是左区间和右区间怎么有序呢,这就可以对左右区间继续划分,当划分到不可分割的子问题时,就可以认为是有序的了,然后对数组进行合并。在合并的过程中,我们需要利用好每个数组都是有序的这个性质,每次在两个数组中取最小的放入新数组,这样整个数组就有序了。


//归并排序

void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) >> 1;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];     //归并,依次对比取小的放进新的临时数组

		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}

	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}

	//拷贝回去
	for (int i = left; i <= right; ++i)
	{
		a[i] = tmp[i];
	}

}

void MergeSort(int* a,int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	_MergeSort(a, 0, n - 1, tmp);
	free(tmp);

}

七.计数排序

计数排序的思想很特别,是一种非比较的排序,他利用了一个有序的数组进行排序。

但是这里的数字和下标的对应不是绝对的,假设数据是100 101 103 106 109,我们这里开辟空间不应该从0开始,不然会浪费空间,我们应该使用相对的映射位置,即num-min,减去最小的那个值。这样的话100就对应在下标0,以此类推。

//计数排序  ,统计出现的次数,在对应的数组上做映射
void CountSort(int* a,int n)
{
	int max = a[0], min = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] > max)
		{
			max = a[i];
		}
		if (a[i] < min)
		{
			min = a[i];
		}
	}

	int range = max - min + 1;
	int* count = (int*)malloc(sizeof(int) * range);
	memset(count, 0, sizeof(int) * range); //初始化次数为0
	for (int i = 0; i < n; i++)
	{
		count[a[i]-min]++;  //统计位置上出现的次数
	}
	int j = 0;
	for (int i = 0; i < range; i++)
	{
		while (count[i]--)
		{
			a[j] = i + min;
			j++;
		}
	}


	free(count);

}

 八.所有源代码

主要的排序算法 · 357c3c3 · jerey/daily code - Gitee.com

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值