排序算法
稳定排序
稳定与非稳定指的是:在一串数列中,…ai…aj…,ai==aj,在经过排序后,他俩的相对位置不变,则为稳定排序。
插入排序
将我们的整个待排序数组分成两个区域,一个叫已排序区,一个叫待排序区。
已排序区在前方,待排序区在后方。
每次将待排序区中的第一个元素取出来,在已排序区中进行比较,逐个交换位置,找到符合条件的位置进行放入。
形象点可以理解为:
- 一个人排队的时候一直往前拱,每次拱的时候,正前方那位向后移动一位(交换)。
- 遇到老大哥的时候害怕了不敢继续往前了,于是就在那呆着了。
举例说明:
2 | 1 | 4 | 9 | 8 | 5 | 3 |
---|
第一轮:
没有已排序的,全是待排序区,第一步将2放入已排序区的队首。
2(已) | 1 | 4 | 9 | 8 | 5 | 3 |
---|
第二轮:
未排序区的队首1 和前面的比较,发现1<2,向前拱
1(已) | 2(已) | 4 | 9 | 8 | 5 | 3 |
---|
第三轮:
未排序区的队首4 和前面的比较,发现前面2比自己小,所以不拱:
1(已) | 2(已) | 4(已) | 9 | 8 | 5 | 3 |
---|
第四轮:
未排序区的队首4 和前面的比较,发现前面4比自己小,所以不拱:
1(已) | 2(已) | 4(已) | 9(已) | 8 | 5 | 3 |
---|
第五轮:
未排序区的队首8 和前面的比较,8<9,向前拱。然后再和前面的4比较,发现前面4比自己小,所以不拱:
1(已) | 2(已) | 4(已) | 8(已) | 9(已) | 5 | 3 |
---|
第六轮:
未排序区的队首5 和前面的比较,5<9,向前拱。然后再和前面的8比较,5<8,向前拱…这里一共交换了两次:
1(已) | 2(已) | 4(已) | 5(已) | 8(已) | 9(已) | 3 |
---|
第七轮:
3<9,交换;3<8,交换…最后成功排序。
代码说明:
/**
* 插入排序
*/
void insert_sort(int* num, int n) {
// 假设将第一个元素看作是已排序区的队首
// 那么后面只需要进行 n - 1 轮排序
for (int i = 1; i < n; ++i) {
// 每一轮排序,都是待排序区的第一个和前面已排序区的进行挪动比较
for (int j = i; j > 0; --j) {
if (num[j] < num[j - 1]) {
swap(num[j], num[j - 1]);
}
}
}
return;
}
冒泡排序
冒泡排序的已排序区在后方,待排序区在前方。
- 一个起指针作用的数指向待排序区的第一个元素
- 每次将待排序区的第一个元素和后面的元素进行比较,如果符合交换条件,则交换,并且指针后移一位
- 当指针循环向后移动到待排序区的最后一个元素时,待排序区里的最值已经挪到了最后一个位置上,形成了已排序区的新的首元素。
现在进行时间复杂度分析:
如果数组是有序的,那么这个头部指针遍历一次后没有交换,一遍就完成了
如果是逆序的,那么操作是n^2
举例说明:
1 | 2 | 5 | 4 | 7 | 3 | 6 | 10 | 8 | 9 |
---|
第一轮:
最后一个元素作为已排序区
1 | 2 | 5 | 4 | 7 | 3 | 6 | 10 | 8 | 9~~(已)~~ |
---|
第二轮:
第一轮:
指针放在未排序区的队首,逐个向后挪动。
在挪动的过程中如果发现指针指向的元素比后面那个元素大,则将元素进行交换。
然后指针继续向后挪动。
1(↓) | 2 | 5 | 4 | 7 | 3 | 6 | 10 | 8 | 9 |
---|
1<2,不交换。
同样,2<5也不交换,现在指针来到第三个位置。
1 | 2 | 5(↓) | 4 | 7 | 3 | 6 | 10 | 8 | 9 |
---|
5>4,进行交换
1 | 2 | 4 | 5(↓) | 7 | 3 | 6 | 10 | 8 | 9 |
---|
5<7,不交换,指针来到第五个位置。
1 | 2 | 4 | 5 | 7(↓) | 3 | 6 | 10 | 8 | 9 |
---|
7>3,进行交换
1 | 2 | 4 | 5 | 3 | 7(↓) | 6 | 10 | 8 | 9 |
---|
指针在第六个位置的时候发现7>6,交换。
然后10和8交换,10再和9交换,最后,10挪到最后一个位置,停止交换,准备开始第二轮。
代码展示:
/**
* 冒泡排序
*/
void bubble_sort(int* num, int n) {
// 冒泡排序也是执行 n - 1 轮
for (int i = 1; i < n; ++i) {
// 每次指针从头开始,随着j的变化,逐个比较并交换两相邻元素
// 需要注意的是,如果传递进来的是10个元素,那么最大的下标为 10 - 1
for (int j = 0; j < n - i ; ++j) {
if (num[j] > num[j + 1]) {
swap(num[j], num[j + 1]);
}
}
}
return;
}
优化
在某轮排序的过程中,如果没有发生交换的操作,这就意味着前面的待排序区已经是有序的状态。
具体的处理操作就是,定义一个times变量,负责记录指针扫描时候的交换次数。
/**
* 优化后的冒泡排序
*/
void bubble_sort(int* num, int n) {
int times = 1;
for (int i = 1; i < n && times; ++i) {
times = 0;
for (int j = 0; j < n - i ; ++j) {
if (num[j] > num[j + 1]) {
swap(num[j], num[j + 1]);
times++;
}
}
}
return;
}
归并排序
首先,假如如果由两个有序数组(n个和m个)合并,我们需要多少次操作呢?
很简单,拿两个指针指向他们的头部,然后再开辟一个新的存储区,如果谁指向的数据小,谁对应的数据就放到存储区中去。
好了,现在再来看看归并排序。
它采用了一种大事化小,小事化了的思想。
- 如果提供了一个长度为10的数组,那我就可以将他们分为两个长度为5的子数组。如果这两个数组已经有序了,那么是否就是上面说的,O(m+n)的合并操作呢?
- 可能长度为5还不够小,所以我们可以继续分,分为长度为[2,3 | 2,3]这四个数组
- 先将两个小数组进行排序,成为了有序数组后再进行合并
现在进行时间复杂度分析:
每一层都对小的数组进行排序,最少进行n次操作。
不难看出,这个划分的过程实现了一个二叉树,树的层高为log2^n。
所以总操作次数为nlog2^n
举例说明:
9 | 7 | 8 | 6 | 4 | 3 | 1 | 2 | 7 | 5 |
---|
首先将数组进行拆分:
A:
9 | 7 | 8 | 6 | 4 |
---|
B:
3 | 1 | 2 | 7 | 5 |
---|
继续拆分:
A:
9 | 7 |
---|
B:
8 | 6 | 4 |
---|
C:
3 | 1 |
---|
D:
2 | 7 | 5 |
---|
然后继续拆分,这里就不再演示。
之后对于每个细分为仅1个或者2个的小数组进行比较排序
排完序后将有序的小数组进行整合,整合为一个大的数组。
代码展示:
/**
* 归并排序
*/
void merge_sort(int* num, int l, int r) {
// 划分后只剩一个元素,直接返回;剩余两个元素,比较后再返回
if (r - l <= 1) {
// 此时划分后的小数组中刚好两个元素
if (r - l == 1 && num[l] > num[r]) {
swap(num[l], num[r]);
}
return;
}
// 计算中间结点,按照中间结点进行数组的划分
int mid = (l + r) >> 1;
merge_sort(num, l, mid);
merge_sort(num, mid + 1, r);
// 开辟空间存放新的排序好的子数组
int* temp = (int*)malloc(sizeof(int) * (r - l + 1));
// 两个子数组合并时
// p1指向左边的起始位置
// p2指向右边的起始位置(r表示的是最右边,mid + 1表示的是右边的起始
// temp指向临时存储区的起始位置
int p1 = l, p2 = mid + 1, k = 0;
while (p1 <= mid || p2 <= r) {
if ((p1 <= mid && num[p1] < num[p2]) || p2 > r) {
temp[k++] = num[p1++];
}
else {
temp[k++] = num[p2++];
}
}
// memcpy(num + l, temp, sizeof(int) * (r - l + 1));
// 上面这句话这样写会好理解一点
// 假设现在是长度[2,2]的子数组合并为4,每次合并完temp后都需要修改原数组的值
// 这样跳回到上一层的递归中时,仍然可以保留修改后的数据值
for (int i = l,j = 0; i <= r; ++i, ++j) {
num[i] = temp[j];
}
free(temp);
}
不稳定排序
选择排序
这里复习一下前面的插入排序。
在插入排序中,未排序区向已排序区中去拱,在逐个交换的过程中放到一排序区中去。
而在选择排序中,这个方向是相反的:
指针指向未排序区的第一个元素,每次遍历一次未排序区,将符合条件的元素和队首去进行一次交换,然后指针后移进入下一轮
代码展示:
/**
* 选择排序
*/
void select_sort(int* num, int n) {
for (int i = 0; i < n; ++i) {
int tempIndex = i;
for (int j = i + 1 ; j < n; ++j) {
tempIndex = (num[j] < num[tempIndex]) ? j : tempIndex;
}
swap(num[i], num[tempIndex]);
}
}
快速排序
- 首先设置一个选择基数
- 进行partition操作,前半段小于基准值,后半段大于等于基准值
举例说明:
5 | 7 | 8 | 6 | 4 | 3 | 1 | 2 | 7 | 5 |
---|
- 首先设置基准值
基准值:[ 5 ]
(head) | 7 | 8 | 6 | 4 | 3 | 1 | 2 | 7 | 5 (tail) |
---|
- 尾指针向前走,找到一个小于5的值,将这个值放入开始的空位置
找到元素[ 2 ],放入头指针位置
2(head) | 7 | 8 | 6 | 4 | 3 | 1 | (tail) | 7 | 5 |
---|
- 此时头指针后移,找到第一个大于基准值的元素,放到尾指针去
2 | (head) | 8 | 6 | 4 | 3 | 1 | 7(tail) | 7 | 5 |
---|
- 再将尾指针前移…当头尾指针重合的时候,将基准值放到这个位置上
- 进行递归操作,对左右两侧递归的使用快速排序
代码展示:
/**
* 快速排序
* 需要两个指针负责控制交换
* 同时对两端的数据进行递归操作
*/
void quick_sort(int* num, int l, int r) {
if (r <= l) return;
int x = l, y = r;
int temp = num[l]; // 设置基准值
// 左右指针没有相遇
while (x < y) {
while (x < y && num[y]>temp) --y;
if (x < y) num[x++] = num[y];
while (x < y && num[x]<temp) ++x;
if (x < y) num[y--] = num[x];
}
// 此时左右指针重合,将基准值放入重合的位置
num[x] = temp;
// 左右递归快排
quick_sort(num, l, x - 1);
quick_sort(num, x + 1, r);
return;
}
查找算法
二分查找
首先的首先,二分查找基于一个有序的数组。
如果arr[mid]>目标值,那么就去左半边查找;否则去右半边
代码展示:
/**
* 二分查找
* 有序数组的查找问题
*/
int binary_search(int* num, int n, int x) {
int head = 0, tail = n - 1, mid;
while (head <= tail) {
mid = (head + tail) >> 1;
if (num[mid] == x) return mid;
if (num[mid] > x)
tail = mid - 1;
else
head = mid + 1;
}
}
这里对二分查找进行一次延伸:
设数组为[1,1,1,1,1,1,1,0,0,0],要找到这个数组中最后一个1所在的下标。
int binary_search(int* num, int n) {
int head = 0, tail = n - 1, mid;
// 这里就不能带等号了
while (head < tail) {
mid = (head + tail) >> 1;
if (num[mid] == 1) head = mid;
else tail = mid - 1;
}
return head;
}