数据结构:(三)排序

〇.写在前面

若读者朋友们发现问题,请不吝斧正。

一.插入排序

(1)直接插入排序

对于一个数组长度为n且有序的数组,我们要插入第n+1个元素,只需要从后往前迭代一次,就肯定能找到对应的位置,完成排序。直接插入排序的实现就基于以上思想。

代码实现如下:

void swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

//直接插入排序
void InsertSort(int* a, int n)
{
	for (int i = 1; i < n; ++i)
	{
		for (int j = i; j > 0; --j)
		{
			if (a[j] < a[j - 1])
			{
				swap(&a[j], &a[j - 1]);
			}
			else
			{
				break;
			}
		}
	}
}

(2)希尔排序

很容易发现,直接插入排序在数组接近有序时复杂度很低。正是基于这样的思想,我们有了希尔排序。

希尔排序是直接插入排序的优化版本,它的思想是:
选取一个整数d作为距离,把数组内下标相隔距离为gap的数归为一组,之后对每一个组进行直接插入排序。之后减小gap的值,重复此过程,直到gap减小为1。

图解如下:
在这里插入图片描述
代码实现如下:

//希尔排序
void _ShellSort(int* a, int n, int gap)
{
	for (int i = gap; i < n; ++i)
	{
		int key = a[i];
		int j = i - gap;
		for (; j >= 0; j -= gap)
		{
			if (a[j] > key)
			{
				a[j + gap] = a[j];
			}
			else
			{
				a[j + gap] = a[j];
				a[j] = key;
				break;
			}
		}
		if (j < 0)
		{
			a[j + gap] = key;
		}
	}
}
void ShellSort(int* a, int n)
{
	int gap = n / 2;
	while (gap > 0)
	{
		_ShellSort(a, n, gap);
		gap /= 2;
	}
}

二.交换排序

(1)冒泡排序

冒泡排序,顾名思义,就是像冒泡泡一样,把最大的那一个数冒泡到数组最后。以此方式选出最大的,次大的…直到最小的,完成排序。

代码实现如下:

void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n - i - 1; ++j)
		{
			if (a[j] > a[j + 1])
			{
				swap(&a[j], &a[j + 1]);
			}
		}
	}
}

(2)快速排序

快速排序是所有排序中综合效率最高的排序方法,其基本思想是:先取数组中一个数作为基准值,通过遍历一次数组,使该数可以位于它最终的位置,且它左边的数都比它小,它右边的数都比它大。再对这个数左边和右边的数组重复此操作,直到排序完成。

或许文字不是那么易懂,有图解如下:(我们实现的是前后指针法):

在这里插入图片描述
前后指针法:
第一步:定义两个下标left,right
分别指向最左边和最右边的元素
第二步:right先走,从后往前,直到找到一个比基准值小的值或者碰到left才停
第三步:left后走,从前往后,直到找到一个比基准值大的值或者碰到right才停
第四步:交换left和right位置的值
第五步:判断left和right是否相遇,
1.如果没有相遇,回到第二步继续循环
2.如果相遇,则交换key和left位置的值,递归到子数组

注意:实现时,如果以最左边的数作为基准值,一定要right先走。由此来保证:在left和right相遇位置的值一定会小于基准值。

虽然快排综合效率很高,但是面对接近有序数组时效率很低,所以我们对快速排序进行三数取中优化:

我们不再取最左边的数作为基准值,而是取三个数作为候选,分别是:最左边的数,最中间的数,最右边的数;从这三个数中取出大小位于中间的数。然后把这个数和最左边的数交换。
图解如下:在这里插入图片描述

代码实现如下:
递归版本:

int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] >= a[mid] && a[left] >= a[right])
	{
		if (a[mid] >= a[right])
			return mid;
		else
			return right;
	}
	else if(a[mid] >= a[left] && a[mid] >= a[right])
	{
		if (a[left] >= a[right])
			return left;
		else
			return right;
	}
	else
	{
		if (a[left] >= a[mid])
			return left;
		else
			return mid;
	}
}

void _QuickSort(int* a, int left, int right)
{
	if (left >= right)
		return;

	int mid = GetMid(a, left, right);
	swap(&a[left], &a[mid]);

	int key = a[left];
	int _left = left;
	int _right = right;

	while (_left < _right)
	{
		//相同也跳过
		while (_left < _right && a[_right] >= key)
			_right--;
		while (_left < _right && a[_left] <= key)
			_left++;

		swap(&a[_left], &a[_right]);
	}

	swap(&a[left], &a[_left]);

	_QuickSort(a, left, _left - 1);
	_QuickSort(a, _right + 1, right);
}

//快速排序
void QuickSort(int* a, int n)
{
	_QuickSort(a, 0, n - 1);
}

非递归版本(c++实现):

void swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

int GetMid(int* a, int left, int right)
{
	int mid = (left + right) / 2;
	if (a[left] >= a[mid] && a[left] >= a[right])
	{
		if (a[mid] >= a[right])
			return mid;
		else
			return right;
	}
	else if (a[mid] >= a[left] && a[mid] >= a[right])
	{
		if (a[left] >= a[right])
			return left;
		else
			return right;
	}
	else
	{
		if (a[left] >= a[mid])
			return left;
		else
			return mid;
	}
}

//快速排序的非递归实现(队列)
void FQuickSort(int* a, int n)
{
	queue<int> q;
	q.push(0);
	q.push(n - 1);

	while (!q.empty())
	{
		int left = q.front();
		q.pop();
		int right = q.front();
		q.pop();

		int mid = GetMid(a, left, right);
		swap(&a[left], &a[mid]);
		int key = a[left];
		int _left = left;
		int _right = right;

		while (_left < _right)
		{
			while (_left < _right && a[_right] >= key)
				_right--;
			while (_left < _right && a[_left] <= key)
				_left++;

			swap(&a[_left], &a[_right]);
		}

		swap(&a[left], &a[_left]);

		if (left < _left - 1)
		{
			q.push(left);
			q.push(_left - 1);
		}
		if (_right + 1 < right)
		{
			q.push(_right + 1);
			q.push(right);
		}
	}
}

三.选择排序

(1)直接选择排序

直接选择排序,顾名思义,从数组中选出最大的元素放在最后面,然后再依次把前n-1,n-2…个数重复上述操作,直到只剩一个数,排序结束。

代码实现如下:

//选择排序
void swap(int* pa, int* pb)
{
	int tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}

void SelectSort(int* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		int max_index = 0;
		int max = a[max_index];
		for (int j = 1; j < n - i; ++j)
		{
			if (a[j] > max)
			{
				max_index = j;
				max = a[j];
			}
		}
		swap(&a[max_index], &a[n - i - 1]);
	}
}

(2)堆排序

要明白堆排序,首先我们需要知道堆是什么。

  • 堆是一棵满二叉树,分为大根堆和小根堆;
  • 大根堆:每一个非叶子节点的值都不小于它孩子节点的值
  • 小根堆:每一个非叶子节点的值都不大于它孩子节点的值

图例如下(来自网络):
在这里插入图片描述
堆的删除:堆只能删除堆顶元素,且删除后需要重新调整,使它仍是一个堆。

之后,我们引入向下调整算法(大堆):

  1. 当左右子树都是堆,但节点本身却不一定满足堆的条件时,就要向下调整
  2. 在自身节点,左孩子节点和右孩子节点中选出最大的个:
    a. 如果最大的那个是自身节点,则函数到此结束
    b. 如果最大的那个是左孩子节点,则交换自身和左孩子节点的值,递归到左孩子节点
    c. 如果最大的那个是右孩子节点,则交换自身和右孩子节点的值,递归到右孩子节点

图解如下:
在这里插入图片描述

建堆:
从最后一个非叶子节点开始到根节点,逐个执行向下调整算法。

图解如下:
在这里插入图片描述

最后,我们就可以排序了

堆排序的基本原理(以大堆排升序为例)就是:

  1. 当传入一个长度为n的待排序的数组,我们先用这个数组构建一个大小为n的大堆
  2. 然后拿出最大值(也就是首元素),和最后一个元素交换,再通过向下调整重新构建一个大小为n-1的堆
  3. 此时我们已经把这n个元素中的最大值放到了数组的最后。再重复2的操作,直到只剩一个元素时,排序就完成了。

图解如下:
在这里插入图片描述

代码实现如下:

void AdjustDown(int* a, int n, int parent)
{
	//左孩子节点索引:lchild = parent * 2 + 1;
	//右孩子节点索引:rchild = lchild + 1;
	int lchild = parent * 2 + 1;
	int rchild = lchild + 1;

	//没有孩子节点,结束递归
	if (lchild >= n)
	{
		return;
	}
	else
	{
		//右孩子存在
		if (rchild < n)
		{
			if (a[parent] >= a[lchild] && a[parent] >= a[rchild])
			{
				return;
			}
			//左孩子最大
			else if (a[lchild] >= a[parent] && a[lchild] >= a[rchild])
			{
				swap(&a[lchild], &a[parent]);
				AdjustDown(a, n, lchild);
			}
			//右孩子最大
			else
			{
				swap(&a[rchild], &a[parent]);
				AdjustDown(a, n, rchild);
			}
		}
		//右孩子不存在
		else
		{
			if (a[parent] >= a[lchild])
				return;
			else
				swap(&a[lchild], &a[parent]);
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆
	//最后一个非叶子节点的坐标index = (n - 1 - 1) / 2;
	int index = (n - 2) / 2;
	for (int i = index; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}

	//排序
	for (int i = n - 1; i > 0; --i)
	{
		swap(&a[0], &a[i]);
		AdjustDown(a, i, 0);
	}
}

(3)TopK问题

如果我们要从一个长度为N数组中找出最大或最小的前K个数,我们应该怎么做?排序,然后选出前K个数。这是一种方法,但我们还有更高效率的方法:

我们要求前K个最小的数:

  1. 建一个大小为K的大堆。
  2. 从第K+1个数开始,比较它和堆顶元素的大小,如果它比堆顶元素小,就把它和堆顶元素交换,然后向下调整;直到最后一个元素。
  3. 最后,这个大堆里的K个数就是我们要找的前K个最小的数。

为什么这种方法可以找到最小的K个数呢?

因为每一个不在这个大堆里的数都比这个大堆的堆顶元素要大,也就比整个堆的元素都要大。所以这个大堆里的元素就是最小的前K个数。

注:如果要找最大的前K个数,建小堆;找最小的前K个数,建大堆。

代码实现:

void TopK(int* a, int n, int k)
{
	//建大堆 得最小K个
	
	//建大小为k的堆
	//最后一个非叶子节点的坐标index = (k - 1 - 1) / 2;
	int index = (k - 2) / 2;
	for (int i = index; i >= 0; --i)
	{
		AdjustDown(a, k, i);
	}

	for (int i = k + 1; i < n; ++i)
	{
		if (a[i] < a[0])
		{
			swap(a[i], a[0]);
			AdjustDown(a, k, 0);
		}
	}

	for (int i = 0; i < k; ++i)
	{
		cout << a[i] << " ";
	}
}

四.归并排序

我们先考虑这样一种情况:
对于两个有序的数组,我们想要把这两个数组合并成一个大的有序数组,我们可以很轻松地完成。

那么,对于无序的数组呢?
我们可以把它从中间二分为两个数组,把这两个数组排序完成,就可以完成对整个数组的排序。

我们发现,这个过程是一个递归的过程,数组可以被细分到只有一个元素,然后向上不断合并,就完成了排序。

这就是归并排序的原理。

图解如下:
在这里插入图片描述
代码实现如下:
递归实现:

//归并排序
void _MergeSort(int* a, int* tmp, int left, int right)
{
	if (left >= right)
		return;

	int mid = (left + right) / 2;
	_MergeSort(a, tmp, left, mid);
	_MergeSort(a, tmp, mid + 1, right);

	int index = left;
	int index1 = left;
	int index2 = mid + 1;
	while (index1 <= mid && index2 <= right)
	{
		if (a[index1] < a[index2])
		{
			tmp[index] = a[index1];
			index++;
			index1++;
		}
		else
		{
			tmp[index] = a[index2];
			index++;
			index2++;
		}
	}

	while (index1 <= mid)
	{
		tmp[index] = a[index1];
		index++;
		index1++;
	}
	while (index2 <= right)
	{
		tmp[index] = a[index2];
		index++;
		index2++;
	}

	memmove(a + left, tmp + left, sizeof(int) * (right - left + 1));

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

非递归实现

//归并排序(非递归)
void FMergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	int gap = 2;
	int step = (gap - 1) / 2;
	while (step < n)
	{
		for (int i = 0; i < n; i += gap)
		{
			int left1 = i;
			int right1 = left1 + step;
			int left2 = right1 + 1;
			int right2 = left2 + step;

			if (right1 >= n)
			{
				continue;
			}
			else if (right2 >= n)
			{
				right2 = n - 1;
			}

			//归并
			int index = left1;
			int index1 = left1;
			int index2 = left2;
			while (index1 <= right1 && index2 <= right2)
			{
				if (a[index1] < a[index2])
				{
					tmp[index] = a[index1];
					index++;
					index1++;
				}
				else
				{
					tmp[index] = a[index2];
					index++;
					index2++;
				}
			}

			while (index1 <= right1)
			{
				tmp[index] = a[index1];
				index++;
				index1++;
			}
			while (index2 <= right2)
			{
				tmp[index] = a[index2];
				index++;
				index2++;
			}

			memmove(a + left1, tmp + left1, sizeof(int) * (right2 - left1 + 1));
		}

		gap *= 2;
		step = (gap - 1) / 2;
	}

	free(tmp);
}

五.稳定性

稳定性是指相同的数在排列完成后相对位置不变,举例如图:
在这里插入图片描述
稳定的排序有:
直接插入排序,直接选择排序,冒泡排序,归并排序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值