排序算法总结
传统排序算法
传统排序算法其时间复杂度一般为 O ( n 2 ) O(n^2) O(n2)。原理简单实现简单。
冒泡排序
| 时间复杂度 | 额外空间复杂度 | 稳定 |
|---|---|---|
| O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | Yes |
for(int i = n - 1; i > 0; i--)
for(int j = 0; j < i; j++)
if(p[j] > p[j + 1])
swap(p[j], p[j + 1])
即每次把最大的元素交换上去,需要交换 n − 1 n-1 n−1次。
性质:
- 有多少个逆序对就有多少次交换被执行。
- 不完全冒泡(指仅仅进行 k k k次冒泡),逆序对数小于等于 k k k的将归零,而大于 k k k的将减去 k k k,每一趟冒泡,总是把当前元素前面最大的交换过去,因此每个元素的逆序数将减一。P6186
对数时间排序算法
对数时间排序算法是基于序理论最优的排序算法,已经被证明不存在基于序理论并且小于对数时间的排序算法,其时间复杂度基本为 O ( n log n ) O(n \log n) O(nlogn)。
快速排序
快速排序是一种基于归并排序的排序算法,对比归并排序,其没有归并的过程,属于自然归并,因其中掺杂随机化算法,因此只讨论期望时间复杂度。
| 时间复杂度 | 额外空间复杂度 | 稳定 |
|---|---|---|
| 期 望 O ( n log n ) 期望O(n \log n) 期望O(nlogn) | O ( n ) O(n) O(n) | No |
过程
与归并排序类似,快速排序也运用了分治的思想。
- 分解:将数组 A [ l … r ] A[l \ldots r] A[l…r]分解成两个部分 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]和 A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r](全文默认为全闭下标集合)。并且我们要求 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]中的元素,都小于等于 A [ m i d ] A[mid] A[mid], A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r]都大于 A [ m i d ] A[mid] A[mid](边界条件不严格,可以根据情况调换)。这个过程的名字叫做分区。
- 解决:通过递归调用,解决两个区间 A [ l … m i d − 1 ] A[l \ldots mid-1] A[l…mid−1]和 A [ m i d + 1 … r ] A[mid+1 \ldots r] A[mid+1…r]的子问题。
- 合并:因为左右两边都是有序的,并且左区间的元素都小于右区间的元素,因此整个数组就是有序的,因此不需要合并问题。
下面的伪代码实现快速排序:
QUICKSORT(A,l,r):
if l < r:
q = PARTITION(A,l,r)
QUICKSORT(A,l,q - 1)
QUICKSORT(A,q + 1,r)
分区
其中 P A R T I T I O N PARTITION PARTITION函数是对数组进行划分,他实现了对数组的重新排序。交换法是实现这个函数的最常用的方法。
PARTITION(A,l,r):
x = A[r]
i = l - 1
for j = p to r - 1
if A[j] <= x
exchange(A[++i],A[j])
exchange(A[++i],A[r])
return i
其中, P A R T I T I O N PARTITION PARTITION函数总是选择 A [ r ] A[r] A[r]作为 A A A的主元,有时也叫哨兵元素。
朴素快速排序的时间复杂度分析
在朴素快速排序中,我们总是选择数组最后一个元素作为主元元素,但是,这样真的最优吗?
考虑特例 A = 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 A = {1,2,3,4,5,6,7,8} A=1,2,3,4,5,6,7,8,第一趟,我们将数组都扫描一遍,递归解决 1 , 2 , 3 , 4 , 5 , 6 , 7 {1,2,3,4,5,6,7} 1,2,3,4,5,6,7,并且依次解决 1 , 2 , 3 , 4 , 5 , 6 {1,2,3,4,5,6} 1,2,3,4,5,6,一直到 1 {1} 1为止,我们发现,每次分割的区间都是一个区间为全满,另一个为空区间,这么做就是退化成插入排序,时间复杂度为 O ( n 2 ) O(n^2) O(n2),不是我们所期望的时间复杂度。
随机化快速排序
根据上面的分析,我们选择主元元素的时候,可以考虑随机的从数组中挑选元素作为主元。因此,我们可以修改分区函数:
RANDOMIZED-PARTITION(A,l,r):
i = RANDOM(l,r)
exchange(A[i],A[r])
PARTITION(A,l,r)
新的排序方法改为调用 R A N D O M I Z E D − P A R T I T I O N RANDOMIZED-PARTITION RANDOMIZED−PARTITION函数即可。
参考《算法导论》第7章,这里给出结论。
随机化快速排序的期望时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)
代码实现
朴素排序:
int partition(int a[], int l, int r)
{
int x = a[r];
int p = l - 1;
for (int i = l; i <= r - 1; i++)
{
if (a[i] <= x)
{
swap(a[i], a[++p]);
}
}
swap(a[r], a[++p]);
return p;
}
void quicksort(int a[], int l, int r)
{
if (l == r)
return;
int p = partition(a, l, r);
quicksort(a, l, p - 1);
quicksort(a, p + 1, r);
}
随机化:
int randomizedPartition(int a[], int l, int r)
{
int k = rand() % (r - l + 1) + l;
swap(a[k], a[r]);
return partition(a, l, r);
}
int partition(int a[], int l, int r)
{
int x = a[r];
int p = l - 1;
for (int i = l; i <= r - 1; i++)
{
if (a[i] <= x)
{
swap(a[i], a[++p]);
}
}
swap(a[r], a[++p]);
return p;
}
void quicksort(int a[], int l, int r)
{
if (l == r)
return;
int p = randomizedPartition(a, l, r);
quicksort(a, l, p - 1);
quicksort(a, p + 1, r);
}
例题
虽然OI中几乎很少让你写快速排序的模板,但是快速排序的思想是很重要的。
寻找第K小的元素,采用快速排序的思想,如果分区大于要求元素,那么继续分治下去;如果分区小于要求元素,那么就需要在另外的那个区域选择,并缩小K的范围。
总之,就是按照分区的情况,合理选择范围。
此算法也称为快速选择算法。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
ll arr[5000005];
int n, k;
int partition(int l, int r)
{
ll x = arr[r];
int j = l - 1;
for (int i = l; i < r; i++)
{
if (arr[i] <= x)
{
swap(arr[i], arr[++j]);
}
}
swap(arr[r], arr[++j]);
return j;
}
ll kth(int l, int r, int k)
{
int p = partition(l, r);
if (p - l == k)
{
return arr[p];
}
else if (p - l > k)
{
return kth(l, p - 1, k);
}
else if (p - l < k)
{
return kth(p + 1, r, k - (p - l) - 1);
}
}
int main()
{
scanf("%d %d", &n, &k);
for (int i = 0; i < n; i++)
{
scanf("%lld", arr + i);
}
printf("%lld", kth(0, n - 1, k));
return 0;
}
求第K小元素的扩展版本,求第1-K小的元素,仍然是快速排序,注意处理两个子区间的时候,注意分区位置 p p p不能包含在左区间中,否则就会出现死循环。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
typedef long long ll;
int n, k;
struct Entry
{
int id;
int ch;
int ma;
int en;
bool operator<(const Entry &o) const
{
if (ch + ma + en == o.ch + o.ma + o.en)
if (ch == o.ch)
return id < o.id;
else
return ch > o.ch;
else
return ch + ma + en > o.ch + o.ma + o.en;
}
} e[305];
int partition(int l, int r)
{
Entry en = e[r];
int j = l - 1;
for (int i = l; i < r; i++)
{
if (e[i] < en)
swap(e[i], e[++j]);
}
swap(e[++j], e[r]);
return j;
}
void akth(int l, int r, int k)
{
if (l > r || k == 0)
return;
int p = partition(l, r);
if (p - l >= k)
{
akth(l, p - 1, k);
}
else
{
akth(l, p - 1, p - l);
printf("%d %d\n", e[p].id, e[p].ch + e[p].en + e[p].ma);
akth(p + 1, r, k - p + l - 1);
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
e[i].id = i + 1;
scanf("%d %d %d", &e[i].ch, &e[i].ma, &e[i].en);
}
akth(0, n - 1, 5);
return 0;
}
线性时间排序算法
线性时间排序算法主要使用了数字的性质,其时间复杂度为 O ( n ) O(n) O(n),可以对值域小的数字进行排序,其一般不稳定。
计数排序
| 时间复杂度 | 额外空间复杂度 | 稳定 |
|---|---|---|
| O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | No |
适合元素范围较小的数组排序,扫描一次数组,并把相应的元素计数器+1,然后按照计数器的数量输出计数器的值即可。
计数排序是桶排序的特例应用。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define FR freopen("in.txt", "r", stdin)
ll arr[1005];
int main()
{
int n, m;
scanf("%d %d", &n, &m);
for (int i = 0; i < m; i++)
{
int id;
scanf("%d", &id);
arr[id]++;
}
for (int i = 1; i <= n; i++)
{
for (int c = 0; c < arr[i]; c++)
{
printf("%d ", i);
}
}
return 0;
}
桶排序
| 时间复杂度 | 额外空间复杂度 | 稳定 |
|---|---|---|
| O ( n σ ( n ) ) O(n \sigma(n)) O(nσ(n)) | O ( n σ ( n ) ) O(n \sigma(n)) O(nσ(n)) | No |
桶排序的概念比较广泛,桶排序的一般思想是将元素放入一个个的桶中,然后在桶内排序,然后将桶排序,类似于捆绑法。 σ ( n ) \sigma(n) σ(n)取决于桶内排序算法
特别的,我们有单桶排序,即一个桶只有单个元素,适用于检查元素完整性。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
bool vis[1001];
int main()
{
int n;
scanf("%d", &n);
int prv = 0;
for (int i = 0; i < n; i++)
{
int val;
scanf("%d", &val);
if (abs(val - prv) <= 1000)
vis[abs(val - prv)] = true;
prv = val;
}
for (int i = 1; i <= n - 1; i++)
{
if (!vis[i])
{
printf("Not jolly");
return 0;
}
}
printf("Jolly");
return 0;
}
本文详细介绍了排序算法,包括传统排序算法如冒泡排序,以及对数时间复杂度的快速排序。重点讲解了快速排序的原理、时间复杂度分析和随机化改进,还提到了线性时间复杂度的计数排序和桶排序。这些排序算法在不同的场景下有不同的效率和适用性。
854

被折叠的 条评论
为什么被折叠?



