笔记 排序(一)

排序

排序是非常常见的任务:有些排序并不直接解决问题,但是可以起到加速作用;有些算法依赖排序。

解决了从小到大排序,就能解决从大到小排序。这是因为我们可以取相反数进行排序。

  • 选择排序

我们重复遍历数组,每次找到当前的最大值并将其加入有序序列的末尾,只要进行 n 次这个数组就是有序的。

void select_sort (int num[], int n) {
	for (int i = 0; i < n; ++i) {
		int pos = 0;
		for (int j = 1; j < n - i; ++j) {
			if (num[j] > num[pos]) pos = j;
		}
		swap(num[pos], num[n - i - 1]);
	}
}
  • 冒泡排序(这个最容易写)

我们重复遍历数组,对于每一次遍历,依次比较相邻的两个元素。若它们的顺序错误,则交换它们的位置。

容易知道,对于每一次遍历,当前最大的元素都会被找到并放在右边的区域。所以在 n 次遍历之后,整个数组一定会是有序的。

代码实现:

void bubble_sort (int num[], int n) {
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < n - i - 1; ++j) {
			if (num[j] > num[j + 1]) swap(num[j], num[j + 1]);
		}
	}
}

代码细节:因为在 k 次遍历后,数组前 k 大的元素都被找出了,因此在内层循环中我们可以不考虑后 k 个元素,即循环边界中的 -i。

我们可以设置一个标记 flag 记录在上一次遍历中是否有元素交换。若没有元素交换,这就说明整个数组已经有序,不用再进行后续的步骤了。

  • 插入排序

类比卢老爷打斗地主的过程,我们不断从牌堆中抽牌,并且维护一个手牌的有序序列,当我们的牌抽完时,我们的手牌就是有序的。具体一点来说,我们要实现“往一个有序序列中插入元素并维持有序性”的操作。

代码实现:

int num[100005], ans[100005], n, tot;

void insert(int k) {
	int pos;
	for (pos = 0; pos < tot; ++pos) {
		if (k <= ans[pos]) break;
	}
	for (int i = tot - 1; i >= pos; --i) {
		ans[i + 1] = ans[i];
	}
	ans[pos] = k;
	++tot;
}

void insert_sort() {
	for (int i = 0; i < n; ++i) {
		insert(num[i]);
	}
}

代码细节:

  1. 对于待排序的每一个元素,执行插入操作
  2. 找到待插入的位置 pos
  3. 将后面的元素全部后移一位
  4. 在 pos 处插入元素

我们注意到,num 数组中待排序的元素个数和 ans 数组中已排序的元素个数的总和就是总元素个数,因为每一个元素不是排过序的就是待排序的。因此,我们可以对代码的空间占用进行优化。具体一点来说,我们正好可以把数组 num 被拿走元素空下来的部分存放有序序列。

代码实现:

void insert(int num[], int tot) {
	int pos, tmp = num[tot];
	for (pos = 0; pos < tot; ++pos) {
		if (tmp <= num[pos]) break;
	}
	for (int i = tot - 1; i >= pos; --i) {
		num[i + 1] = num[i];
	}
	num[pos] = tmp;
}

void insert_sort(int num[], int n) {
	for (int i = 0; i < n; ++i) {
		insert(num, i);
	}
}

注意到我们改变了函数的接口,这种写法的优势更大:它可以使函数的泛用性更好,继而可以更方便的使用和更省力的维护。

代码复用(code reuse):指的是我们尽可能重复使用自己已经写过的代码。(单一责任原则、封装)想要减少调试时间,要注意代码的可复用性。

  • 复杂度

程序运行需要使用时间和空间,我们使用时间复杂度空间复杂度来描述一个算法。时间复杂度一般用 O 记号分析数量级,空间复杂度一般可以算出精确值。

这三种算法其实都很慢。它们的时间复杂度为 O(n^{2})

已经证明:基于比较的排序,它们的复杂度最好也只能做到 O(nlogn),下面是两种达到这种复杂度的排序算法:

  • 归并排序

归并排序是基于分治思想的排序方法。对于一个待排序数组,我们把它分成左半与右半两个部分,并分别对它们进行归并排序。最后我们把两个有序数组归并为一整个有序数组。

归并有序数组 A 和 B 的方法为:

  1. 若 A 和 B 中都有剩余元素,将它们头部较小的元素取出放入有序序列
  2. 若 A 中没有剩余元素,将 B 中所有元素按顺序加入序列
  3. 若 B 中没有剩余元素,将 A 中所有元素按顺序加入序列

以上是双路归并的步骤,实际上我们也可以将数组划分成多个部分进行多路归并,它们的原理是一致的。

代码实现:

void merge (int num[], int left, int mid, int right) {
	int pointer1 = left, pointer2 = mid, tot = 0;
	static int helper[100005];
	while (pointer1 < mid && pointer2 < right) {
		if (num[pointer1] >= num[pointer2]) helper[tot++] = num[pointer2++];
		else helper[tot++] = num[pointer1++];
	}
	while (pointer1 < mid) helper[tot++] = num[pointer1++];
	while (pointer2 < right) helper[tot++] = num[pointer2++];
	for (int i = 0; i < tot; ++i) {
		num[left + i] = helper[i];
	}
}

void merge_sort (int num[], int left, int right) {
	if (left + 1 == right) return;
	int mid = (left + right) / 2;
	merge_sort(num, left, mid);
	merge_sort(num, mid, right);
	merge(num, left, mid, right);
}

代码细节:我们的区间一般都是左闭右开区间,为了实现归并,我们设置两个指针分别指向两个有序数组的头部。我们还需要一些辅助空间存放有序数组并在最后将其复制进原数组。

代码主要是 merge 函数比较消耗时间,merge 函数的时间复杂度为 O(n)。我们设对长度为 n 的数组进行归并的时间复杂度为 T(n),那么我们有:

                                                        T(n) = 2 * T(n/2) + O(n)

根据主定理,时间复杂度为 O(nlogn)

  • 快速排序

快速排序的思想是:我们每次选择数组中的一个“哨兵”(通常指被拿来和其他东西作比较的元素),把比这个数小的放到它左边,把比这个数大的放到它右面,这样我们就有了“数组有序”的一个必要条件。如果对于每一个元素我们都有这种性质,那么这个数组就是有序的。

显而易见,哨兵的选择很有讲究。如果我们每一固定把数组的首个元素作为哨兵,那么对于单调递减的数组,这个算法会退化成为 O(n^{2})的。为了保证效率,我们采用随机化的方式选择哨兵。

有一点需要注意:我们只考虑了“比哨兵大”和“比哨兵小”两种元素并对它们递归处理。而对于和哨兵相等的元素,我们没有必要让它们进入递归,只要直接复制即可,这实际上是进行了一次三分。

void quick_sort(int num[], int left, int right) {
	if (right - left <= 1) return;
	static int helper[10005];
	int guard = num[left + (rand() % (right - left))], pointer1 = left, pointer2 = right;
	for (int i = left; i < right; ++i) {
		if (guard > num[i]) helper[pointer1++] = num[i];
		else if (guard < num[i]) helper[--pointer2] = num[i];
	}
	for (int i = left; i < pointer1; ++i) {
		num[i] = helper[i];
	}
	for (int i = pointer2; i < right; ++i) {
		num[i] = helper[i];
	}
	for (int i = pointer1; i < pointer2; ++i) {
		num[i] = guard;
	}
	quick_sort(num, left, pointer1);
	quick_sort(num, pointer2, right);
}
  • 额外的评价标准

稳定排序:如果在排序前后,具有相同关键字的对象之间的相对次序不变,那么这个排序算法是稳定的,反之是不稳定的。

原地排序:不申请多余的存储空间的排序算法称为原地排序。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值