详解——八大排序(十分钟带你吃透)

目录

0. 前言

1. 插入排序​

2. 希尔排序

3. 冒泡排序

4. 1快速排序(hoare版本)

4. 2快速排序(挖坑法)

4. 3快速排序(前后指针法)

 4.4快速排序(非递归法)

5. 选择排序

6. 堆排序

7. 1 归并排序(递归实现)

7. 2 归并排序(非递归实现)

8. 计数排序

9.八大排序时间复杂度


0. 前言

众所周知,对于顺序表,以二分法查找一个数,算法时间复杂度为O(nlongn)。因而,一次排好序,便可以节约很多查找的时间。

由此可见,排序算法尤为重要。以下介绍八大排序算法,都是从小到大排序。


1. 插入排序

i从1n-1,每次将第i位的数插入到前面已排好序的序列中。

插入的方法为:将i与i-1位置的数比较,i小则交换并i--,i大则停止插入。

// 插入排序
// 时间复杂度:O(N^2)  -- 逆序
// 最好 O(N) -- 顺序有序 或 接近顺序有序
void InsertSort(int* a, int n)
{
	for (int i = 0; i < n - 1; i++)
	{
		//单趟排序
		int end = i;
		int tmp = a[end + 1];
		while (end >= 0)
		{
			//5  1  3  7   8  2
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
				break;
		}
		a[end + 1] = tmp;
	}
}

2. 希尔排序

将以5 3 1为增量的序列依次进行插入排序。例如,增量为5时,对(a0,a5...)、(a1,a6...)、(a2,a7...)、(a3,a8...)、(a4,a9...)序列依次进行插入排序。

// 希尔排序
// 平均:O(N^1.3)
void ShellSort(int* a, int n)
{
    // 1、gap > 1  预排序
	// 2、gap == 1 直接插入排序
	int gap = n;
	while (gap > 1)
	{
		gap = gap / 3 + 1;
		for (int i = 0; i < n - gap; i++)
		{
			//8        2       6       5          7   4  3
			//end                   end+gap
			int end = i;
			int tmp = a[end + gap];
			while (end >= 0)
			{
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;
				}
				else
					break;
			}
			a[end + gap] = tmp;
		}
	}
}

3. 冒泡排序

每次从0到i序列的数中挑取最大的数放在i位置,i从n-10

挑取最大数的过程为:将j位置数与j+1位置比较,j大则交换,小不交换,j++。

// 冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		int exchange = 0;
		for (int j = 1; j < n - i; j++)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
				exchange = 1;
			}
		}
        //如果单趟走完,已经有序,那么就不需要再冒了
		if (exchange == 0)
		{
			break;
		}
	}
}

4. 1快速排序(hoare版本)

遍历一次数组,以第一个数为标准,分成三部分,实现:第一个数在中间,小于它的数在它前面,大于它的数在它后面。然后对前部分和后部分递归该操作,递归终点为数组中个数为1。此处要注意的是,中间部分的数不需要再递归操作。

快排核心

遍历一遍数组,空间复杂度为O(1),将数组分为三部分的方法为:

  • 1)取出第一个数下标暂存key,设定left和right分别指向数组头和尾
  • 2)right先走,直到找到比第一个数小
  • 3)然后left走,直到找到比key大的数
  • 4)  将left和right位置的数字交换
  • 其中,要时刻保持left<right
  •  
  • 走到最后left和right会相遇,将key和left(或者right)交换即可
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
	int key = left;

	while (left < right)
	{

		//右边找比key小的
		while (left < right && a[right] >= key)
		{
			--right;
		}

		//左边找比key大的
		while (left < right && a[left] <= key)
		{
			++left;
		}

		Swap(&a[right], &a[left]);
	}
	//跳出循环说明相遇了
	Swap(&a[key], &a[left]);

	return left;
}

4. 2快速排序(挖坑法)

定义两个指针left指向起始位置,right指向最后一个元素的位置

然后指定一个坑(pit),right寻找比key 小的数字

找到后将right的数据赋给pit,right成为一个坑

然后left寻找比key 大的数字,找到将left的数据赋给pit,left成为一个新坑,循环这个过程,直到right 与left 相遇,然后将key返回给那个坑(最终:key的左边都是比key小的数,key的右边都是比key大的数),然后进行递归操作。
 


// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
	int key = a[left];
	//坑位
	int pit = left;

	while (left < right)
	{
		//右边先走,找小
		while (left < right && a[right] >= key)
		{
			right--;
		}

		a[pit] = a[right];
		pit = right;

		//左边走,找大
		while (left < right && a[left] <= key)
		{
			left++;
		}

		a[pit] = a[left];
		pit = left;
	}
	a[pit] = key;
	return pit;
}

4. 3快速排序(前后指针法)

定义两个指针,一前一后,pre(前)指向起始位置,cur(后)指向pre的后一个位置;
实现过程:cur找比keyi小的数,同时++pre != cur,那么就交换cur和pre的数

++pre==cur,那么就++cur
直到cur走到right前一个位置,终止循环。最后++prev,交换pre和keyi的值。返回中间位置pre。最后再继续递归。

 

//三数取中(前中后)
int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
			return right;
	}
	else  //a[left]>a[mid]
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] > a[left])
		{
			return left;
		}
		else
			return mid;
	}
}


// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
    //当数字在有序或者接近有序的时候上面的方法效率会非常低,我们可以用 三数取中法 解决。
	int midi = GetMidIndex(a, left, right);


	Swap(&a[midi], &a[left]);

	int keyi = left;
	int pre = left;
	int cur = left + 1;
	//pre   cur   
	//5      3     2      7     4
	while (pre <= cur)
	{
		if (a[cur] < a[keyi] && a[++pre] != a[cur])
		{
			Swap(&a[cur], &a[pre]);
		}
		cur++;
	}

	Swap(&a[keyi], &a[pre]);

	return pre;
}

 4.4快速排序(非递归法)

用栈实现

先将数组的left和right入栈(left right)

然后分别出栈,作为数组的end和begin

调用三大快排法之一,取到keyi

从keyi分成两个区间[[begin, keyi-1]    [keyi+1, end]

 

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
	ST st;
	StackInit(&st);
	StackPush(&st, left);
	StackPush(&st, right);

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

		int keyi = PartSort3(a, begin, end);
		//左区间            右区间
		//[begin, keyi-1]  [keyi+1,end]

		if (begin < keyi - 1)
		{
			StackPush(&st, begin);
			StackPush(&st, keyi - 1);
		}

		if (keyi + 1 < end)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, end);
		}


	}


	/*销毁栈*/
	StackDestory(&st);
}

5. 选择排序

每次从0到n-1个数中选最小的数放在最前面,最大的数放在最后面。

选完之后left--

                right++

// 选择排序
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;
	while (left < right)
	{
		int mini = left;
		int maxi = left;
		//每次排完之后最小值在前面,最大值在后面,所以i要小于right
		for (int i = left + 1; i <= right; i++)
		{
			if (a[i] < a[mini])
			{
				mini = i;
			}
			if (a[i] > a[maxi])
			{
				maxi = i;
			}
		}
		Swap(&a[left], &a[mini]);
		//9        1      5     3   4   2
		//maxi    mini
//交换之后1        9
		//maxi    mini
		//小标出现重合,需要特殊处理
		if (left == maxi)
		{
			maxi = mini;
		}
		Swap(&a[right], &a[maxi]);
		
		left++;
		right--;
	}
}

6. 堆排序

堆排序,又称完全二叉树排序。是将数组看作一颗完全二叉树,数组i位置的左右孩子分别为2*i+12*i+2位置。

排序过程为:

  • 1)建堆:从最后一个往第一个,逐个进行“筛选”,即建成最大顶堆;
  • 2)排序:将堆顶取出,与最后一个进行交换,然后再对第一个点进行“筛选”,此次“筛选”数组长度减一;
  • 筛选:对某个结点筛选,即当该结点两个孩子中存在值比该结点大的,就交换位置。然后重复操作,直到结点的两个孩子的值都比该结点小,或孩子为空。

 

// 堆排序
void AdjustDwon(int* a, int n, int root)
{
	int parent = root;
	//默认左孩子
	int child = parent * 2 + 1;
	while (child < n)
	{

		//选出左右孩子中小的那一个
		//a[child+1]>a[child]  建大堆
		if (child + 1 < n && a[child + 1] < a[child])
		{
			child++;
		}

		//如果孩子比父亲小,则交换,继续向下调整
		//a[child]>a[parent]  建大堆
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;

		}
		else
		{
			break;
		}
	}
}
void HeapSort(int* a, int n)
{
	// 向上调整--建堆 O(N*logN)
	//for (int i = 1; i < n; ++i)
	//{
	//	AdjustUp(a, i);
	//}

	// 向下调整--建堆 O(N)
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}


	size_t end = n - 1;
	while (end > 0)
	{
		//首尾交换
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

7. 1 归并排序(递归实现)

对于一段数组先分两半,各自进行归并排序,再将两半部分内容有序地合并起来,此时只需各遍历一次即可,采用递归实现。递归的终结点是数组中只有一个数时则不需要进行上述操作。

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
		return;

	int mid = begin + (end - begin) / 2;
	//[begin,mid]  [mid+1,end]
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	//归并
	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int tmpi = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
		tmp[i++] = a[begin1++];

	while (begin2 <= end2)
		tmp[i++] = a[begin2++];
	

	//将tmp内容拷贝到原数组中
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
// 归并排序递归实现
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

7. 2 归并排序(非递归实现)

将数组分为gap组,每次gap组内先排序然后再合并

通过拷贝到tmp数组内然后再拷贝回原数组

// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	//组
	int gap = 1;

	while (gap < n)
	{
		//间距为gap是一组,两两归并
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			//存在越界的情况,含有gap的都有可能越界

			//end1越界
			if (end1 > n)
			{
				end1 = n - 1;
			}

			//begin2 越界,说明第二个区间不存在
			if (begin2 > n)
			{
				//begin2>end2
				begin2 = n;
				end2 = n - 1;
			}

			//begin2 存在但end2越界
			if (begin2 < n && end2 >= n)
			{
				end2 = n - 1;
			}

			int tmpi = begin;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[i++] = a[begin1++];
				}
				else
				{
					tmp[i++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
				tmp[i++] = a[begin1++];

			while (begin2 <= end2)
				tmp[i++] = a[begin2++];

		}
		memcpy(a, tmp, n * sizeof(int));

		gap *= 2;
	}
	free(tmp);
}

 

8. 计数排序

在所有要排序的数据中找到最大的数,求出它的位数n。

建立编号从0到9的十个队列。

i从最高位n到最低位1。

遍历数组将第i位为0的依次放入编号为0的队列中,第i位为1的放入编号为1的队列中。。。

所有数入相应队列后,再依次将0到9编号队列中数取出放回数组中。

然后对低一位重复该操作。

// 计数排序
// 6  5  6  7   4   5  10
// 2  2     1   1       1
void CountSort(int* a, int n)
{
	int min = a[0];
	int max = a[0];
	for (int i = 0; i < n; i++)
	{
		if (a[i] < min)
			min = a[i];

		if (a[i] > max)
			max = a[i];
	}

	int range = max - min - 1;
	int* count = (int*)malloc(sizeof(int) * range);
	assert(count);
	//初始化数组
	memset(count, 0, sizeof(int) * range);

	//计数
	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;
		}
	}
}

9.八大排序时间复杂度

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值