目录
POJ 2299 Ultra-QuickSort(归并排序)
一,排序算法
1,分类方法
(1)内部排序、外部排序
根据排序数据来自内部存储器还是外部存储器分类的,现在一般都是内部排序,外部排序的场景很少。
(2)原址排序、非原址排序
原址排序一般只需要O(1)的额外空间,包括快速排序、选择排序、插入排序、冒泡排序、希尔排序
非原址排序需要一定的额外辅助空间,包括归并排序、堆排序
(3)稳定排序、非稳定排序
相等元素排序之后的相对次序和原次序相同的排序叫稳定排序,包括插入排序,基数排序,归并排序,冒泡排序,计数排序
否则叫非稳定排序,包括快速排序,希尔排序,选择排序,堆排序
桶排序是一种嵌套排序算法,是否是稳定排序取决于内部嵌套的是哪种排序算法。
(4)基于计算的排序算法、基于比较的排序算法
计数排序、基数排序、桶排序是基于计算的排序算法,其他常见排序算法都是基于比较的。
基于比较的排序算法,适用于任何良序集,只需要自定义cmp函数(如下面代码所示),即可排序,
也就是说,无论待排序的元素是整数,实数甚至复数,无论是字符串,结构体,还是vector等等,只要能定义出合适的cmp函数使得待排序元素构成的集合是良序的即可。
而基于计算的排序算法,基本上只适用于元素是整数或者实数。
当然,对于结构体,如果只根据其中一个成员作为比较对象,其实也能用基于计算的排序算法,
但是,用来比较的对象还是只能是整数或者实数,而不像基于比较的算法都支持结构体用多个成员作为比较对象,
而且,计数排序只能对特殊的结构体排序,即满足如果两个结构体变量的key成员相同那么两个结构体就完全相同的结构体,其中key成员指的是用来排序的成员。
接下来准备花点时间,把常见排序算法的代码都写一写。
2,排序算法测试代码
(1)提供模板函数
template<typename T>
bool cmp(T a, T b)
{
return a < b;
}
template<typename T>
void exchange(T* a, T* b)
{
T tmp = *a;
*a = *b;
*b = tmp;
}
template<typename T>
T GetMax(T* arr, int len)
{
T ans = arr[0];
for (int i = 1; i < len; i++)if(cmp(ans,arr[i]))ans=arr[i];
return ans;
}
这3个基础函数,可以用于各排序算法中。
(2)约定接口
基于比较的排序算法写成泛型接口,提供2个接口版本,即cmp函数指针可以省略
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
}
template<typename T>
void Sort(T* arr, int len)
{
Sort(arr,len,cmp);
}
基于计算的排序算法直接以int类型作为排序对象类型,以普通小于作为比较函数
void Sort(int* arr, int len)
{
}
(3)排序功能测试
搭建一个简单的评测系统,用来判断排序算法是否完成了功能:
#include <iostream>
using namespace std;
int main() {
const int N = 1000;
int arr[N];
for (int i = 0; i < N; i++)arr[i] = rand()%10000;
Sort(arr, N);
for (int i = 0; i < N - 1; i++) {
if (arr[i] > arr[i + 1]) {
cout << "error! arr[i] = " << arr[i] << ", arr[i+1] = " << arr[i + 1] << endl;
}
}
cout << "end";
return 0;
}
然后就可以开始写排序算法了。
(4)排序稳定性测试
struct Node {
int a;
int b;
bool operator<(const Node &x) const{
return a < x.a;
}
};
template<typename T>
void StableSort(T* arr, int len, bool(*cmp)(T a, T b))
{
for (int i = 1; i < len; i++) {
for (int j = i; j > 0; j--) {
if (cmp(arr[j], arr[j - 1]))exchange(arr + j, arr + j - 1);
else break;
}
}
}
template<typename T>
void StableSort(T* arr, int len)
{
StableSort(arr, len, cmp);
}
void testStable()
{
const int N = 12345;
Node arr[N];
Node arr2[N];
for (int i = 0; i < N; i++)arr[i].a = rand() % 1000, arr[i].b = rand(), arr2[i] = arr[i];
Sort(arr, N);
StableSort(arr2, N);
bool flag = true;
for (int i = 0; i < N; i++)if (arr[i].b != arr2[i].b)flag = false;
if (flag)cout << "is stable.";
else cout << "not stable";
}
3,常见排序算法
(1)插入排序
算法思路:先让前i个元素是有序的,然后插入一个元素,让前i+1个元素是有序的。
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
for(int i=1; i<len; i++){
for(int j=i; j>0; j--){
if(cmp(arr[j], arr[j-1]))exchange(arr+j, arr+j-1);
else break;
}
}
}
最坏时间:O(n^2)
平均时间:O(n^2)
稳定性测试结果:稳定
PS:插入排序的时间其实就是Θ(n+逆序对的数目)
(2)选择排序
算法思路:选择未排序的所有元素中的最小元素,直接放到已排序的这一段的后面。
template<typename T>
int getMinId(T* arr, int len, bool(*cmp)(T a, T b))
{
int ans=0;
for(int i=1;i<len;i++){
if(cmp(arr[i], arr[ans]))ans=i;
}
return ans;
}
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
for(int i=0; i<len; i++){
int minId = getMinId(arr+i, len-i, cmp);
exchange(arr+i, arr+i+minId);
}
}
最坏时间:O(n^2)
平均时间:O(n^2)
稳定性测试结果:不稳定
(3)冒泡排序
算法思路:先把n个元素依次扫描一遍,相邻俩元素排序一下,扫描完之后最大元素就会出现在最后的位置,
然后再扫描前n-1个元素...依次类推
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
for(int i=len-1; i>0; i--){
for(int j=0;j<i;j++){
if(cmp(arr[j+1], arr[j]))exchange(arr+j+1, arr+j);
}
}
}
最坏时间:O(n^2)
平均时间:O(n^2)
稳定性测试结果:稳定
或者对称的写法:
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
for (int i = 0; i < len; i++) {
for (int j = len - 1; j > i; j--) {
if (cmp(arr[j], arr[j - 1]))exchange(arr + j - 1, arr + j);
}
}
}
时间复杂度和稳定性不变
(4)双向冒泡排序
也叫鸡尾酒排序,就是把2个方向的冒泡排序结合一下。
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
for (int low = 0, high = len - 1; low < high; low++, high--) {
for (int j = high; j > low; j--) {
if (cmp(arr[j], arr[j - 1]))exchange(arr + j - 1, arr + j);
}
for (int j = low; j < high; j++) {
if (cmp(arr[j + 1], arr[j]))exchange(arr + j + 1, arr + j);
}
}
}
最坏时间:O(n^2)
平均时间:O(n^2)
稳定性测试结果:稳定
(5)归并排序
归并排序主要是Merge操作和递归操作。
template<typename T>
void Merge(T* arr1, int len1, T* arr2, int len2, bool(*cmp)(T a, T b))
{
T* arr = new T[len1 + len2];
int i, j;
for (i = 0, j = 0; i < len1 && j < len2;) {
if (cmp(arr2[j], arr1[i]))*arr = arr2[j++];
else *arr = arr1[i++];
arr++;
}
while(i<len1)*(arr++) = arr1[i++];
while(j<len2)*(arr++) = arr2[j++];
arr -= len1 + len2;
for (int i = 0; i < len1 + len2; i++)arr1[i] = arr[i];
}
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
if (len <= 1)return;
Sort(arr, len / 2, cmp);
Sort(arr + len / 2, len - len / 2, cmp);
Merge(arr, len / 2, arr + len / 2, len - len / 2, cmp);
}
最坏时间:O(nlogn)
平均时间:O(nlogn)
稳定性测试结果:稳定
Merge操作合并2个数组的过程中,包含至多len次cmp操作和2*len次赋值操作,其中len=len1+len2表示2个数组的长度和
(6)堆排序
代码来自:二叉堆、堆排序_nameofcsdn的博客-CSDN博客
int LeftChild(int id)
{
return id * 2 + 1;
}
int RightChild(int id)
{
return id * 2 + 2;
}
template<typename T>
void AdjustHeap(T* arr, int rootId, int size, bool(*cmp)(T a, T b))
{
int largest = rootId, left = LeftChild(rootId), right = RightChild(rootId);
if (left < size && cmp(arr[largest], arr[left]))largest = left;
if (right < size && cmp(arr[largest], arr[right]))largest = right;
if (largest == rootId)return;
exchange(arr + rootId, arr + largest);
AdjustHeap(arr, largest, size, cmp);
}
template<typename T>
void InitHeap(T* arr, int size, bool(*cmp)(T a, T b))
{
for (int i = size / 2; i >= 0; i--)AdjustHeap(arr, i, size, cmp);
}
template<typename T>
void Sort(T* arr, int size, bool(*cmp)(T a, T b))
{
InitHeap(arr, size, cmp);
for (int i = size - 1; i > 0; i--) {
exchange(arr + i, arr);
AdjustHeap(arr, 0, i, cmp);
}
}
最坏时间:O(n log n)
平均时间:O(n log n)
稳定性测试结果:不稳定
(7)快速排序
不稳定版:
template<typename T>
void Sort(T* arr, int low, int high, bool(*cmp)(T a, T b))
{
if (low >= high)return;
T x = arr[high];
int id = high;
for (int i = low; i < id;) {
if (cmp(arr[i], x))i++;
else exchange(arr + i, arr + id--);
}
arr[id] = x;
Sort(arr, low, id - 1,cmp);
Sort(arr, id + 1, high, cmp);
}
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
Sort(arr, 0, len - 1, cmp);
}
最坏时间:O(n^2)
平均时间:O(n log n)
稳定性测试结果:不稳定
稳定版:
template<typename T>
void Sort(T* arr, T* arr2, int low, int high, bool(*cmp)(T a, T b))
{
if (low >= high)return;
int id = low, id2, id3;
T x = arr[high];
for (int i = low; i < high; i++) {
if (cmp(arr[i], arr[high]))arr2[id++] = arr[i];
}
id2 = id;
for (int i = low; i < high; i++) {
if (!cmp(arr[i], arr[high]) && !cmp(arr[high], arr[i]))arr2[id++] = arr[i];
}
arr2[id++] = x;
id3 = id;
for (int i = low; i < high; i++) {
if (cmp(arr[high], arr[i]))arr2[id++] = arr[i];
}
for (int i = low; i <= high; i++)arr[i] = arr2[i];
Sort(arr, arr2, low, id2 - 1, cmp);
Sort(arr, arr2, id3, high, cmp);
}
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
T* arr2 = new T[len];
Sort(arr, arr2, 0, len - 1, cmp);
}
最坏时间:O(n^2)
平均时间:O(n log n)
稳定性测试结果:稳定
(8)基数排序
适应场景:假设排序对象全是自然数
void Sort(int* arr, int len, int key)
{
int** p = new int*[10];
for(int i=0;i<10;i++)p[i]=new int[len];
int num[10]={0};
for(int i=0;i<len;i++){
int val = arr[i]/key%10;
p[val][num[val]++] = arr[i];
}
for(int i=0;i<10;i++){
for(int j=0;j<num[i];j++)*(arr++)=p[i][j];
}
}
void Sort(int* arr, int len)
{
int m = GetMax(arr,len);
for(int key=1;key<=m;key*=10){
Sort(arr,len,key);
}
}
最坏时间:O(kn),其中k是最大自然数的10进制的位数
平均时间:O(kn)
稳定性测试结果:用上面的测试代码测不了,实际上是不稳定。
怎么理解这个时间复杂度呢?
从一个角度看,k不会超过10,所以可以理解为O(n)但是常数比较大。
从另外一个角度看,在很多场景下,最大数的数值是比n要大的,那么kn实际上比nlogn还大。
总之,无论如何都有一个共识,虽然看似是O(n)的复杂度,但是常数很大,很多时候很慢。
如果是像我代码这样,每次都要new,那就更慢了,这一步可以优化,但是优化完也比其他很多排序算法慢。
(9)计数排序
适应场景:假设排序对象全是自然数,而且最大数不会太大
void Sort(int* arr, int len)
{
int m = GetMax(arr, len);
int* pnum = new int[m + 1];
for (int i = 0; i <= m; i++)pnum[i] = 0;
for (int i = 0; i < len; i++)pnum[arr[i]]++;
for (int i = 0; i <= m; i++) {
for (int j = 0; j < pnum[i]; j++)*(arr++) = i;
}
}
最坏时间:O(m+n),其中m是最大自然数
平均时间:O(m+n)
稳定性测试结果:用上面的测试代码测不了,实际上是不稳定。
(10)桶排序
算法思路:先把所有数分成若干个桶,然后每个桶调用别的排序算法进行排序,组装起来就是完整的排序了。
适用场景:待排序元素都是实数,但是这里我们用都是自然数为例来实现代码。
void Sort(int* arr, int len)
{
int m = GetMax(arr, len);
const int T = 10; //大于1的任何数都行
int gap = m / T + 1; //桶的范围大小
int num[T] = { 0 };
for (int i = 0; i < len; i++)num[arr[i] / gap]++;
int id[T] = { 0 };
for (int i = 1; i < T; i++)id[i] = id[i - 1] + num[i - 1];
int* p = new int[len];
for (int i = 0; i < len; i++)p[id[arr[i] / gap]++] = arr[i];
int s = 0;
for (int i = 0; i < T; i++) {
Sort2(p + s, num[i]);
s += num[i];
}
for (int i = 0; i < len; i++)arr[i] = *(p++);
}
其中的sort2是调用别的排序算法,比如算法导论中提到的是插入排序,
甚至这个函数可以改一改,调用本身递归也是可以完成排序的(只针对整数的情况,对实数没法完成排序),这样其实就基本和基数排序一样了,只不过顺序反过来的,
基数排序是先按照低位(个位)排一次,然后再高位排,这里如果调用自身完全排序,那就是先按照高位排,然后再低位排。
我这里是用计数的方法,用一个数组p依次存下了所有的桶,实际上用链表或者vector来存这些桶更方便。
之所以选择插入排序是因为,插入排序在对几乎已经排好序的数据操作时效率高。
如果分桶之后嵌套的是插入排序,那么时间复杂度是:
最坏时间:O(n^2)
平均时间:O(n + n^2 / T)其中T是桶的数量,当T=n时,平均时间是O(n)
这个平均时间的计算比较复杂,算法导论上是用指示器随机变量来算的。
(11)希尔排序
(12)近似计数排序
计数排序只能处理整数的情况,如果用关联容器,可以处理能多的类型。
template<typename T>
class fun
{
public:
bool operator()(T a, T b) const
{
return cmp(a,b);
}
};
template<typename T>
void Sort(T* arr, int len, bool(*cmp)(T a, T b))
{
map<T, int, fun<T>>m;
for (int i = 0; i < len; i++)m[arr[i]]++;
for (auto& it : m) {
for (int j = 0; j < it.second; j++) *(arr++) = it.first;
}
}
把cmp函数封装成了函数对象。
耗时主要在map的插入操作,log1 + log2 + log3 + ... + logn = n logn
所以时间复杂度是O(n logn)
二,排序算法效率、组合嵌套
评价一个排序算法的时间效率,主要看最坏时间、平均时间、常数
比如归并排序的最坏时间是O(nlogn),但是常数大,插入排序的最坏时间是O(n^2),但是常数小,所以数组较小时用插入排序可能更快。
又比如快速排序的平均时间是同类算法中最快的,但是最坏时间是O(n^2)。
因为有这2种情况的存在,所以把两种及以上的排序算法进行组合嵌套,能达到更好的效果。
而桶排序、希尔排序,本身就是嵌套了其他排序算法的排序算法。
算法导论习题:
答:总时间是Θ(k^2)* n/k = Θ(nk)
答:上面提到,Merge操作合并2个数组的时间是Θ(len),其中len=len1+len2表示2个数组的长度和
n/k个数组需要log(n/k) / log2轮合并,每一轮的时间都是Θ(n),所以总时间是Θ(nlog(n/k))
答:log n
答:这个主要就看各个常数了,这个比较复杂,不过k应该是不超过10的
三,OJ实战
链表适合插入排序,统计逆序数适合归并排序。
力扣 147. 对链表进行插入排序(插入排序)
对链表进行插入排序。
插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。
每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。
插入排序算法:
插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。
示例 1:
输入: 4->2->1->3
输出: 1->2->3->4
示例 2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
思路一:插入排序
class Solution {
public:
ListNode* insertionSortList(ListNode* head) {
if(!head)return NULL;
ListNode* p=head;
while(p && p->next){
ListNode*pn=p->next;
if(p->val<=pn->val){
p=pn;
continue;
}
p->next=pn->next;
if(pn->val<head->val)pn->next=head,head=pn;
else{
ListNode* h =head;
while(h->next->val<pn->val)h=h->next;
pn->next=h->next,h->next=pn;
}
}
return head;
}
};
思路二:
直接调用我的ListSort函数模板:ACM模板
力扣 148. 排序链表(插入排序)
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
进阶:
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
示例 1:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
示例 2:
输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:
输入:head = []
输出:[]
提示:
链表中节点的数目在范围 [0, 5 * 104] 内
-105 <= Node.val <= 105
思路一:插入排序
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head)return NULL;
ListNode* p=head;
while(p && p->next){
ListNode*pn=p->next;
if(p->val<=pn->val){
p=pn;
continue;
}
p->next=pn->next;
if(pn->val<head->val)pn->next=head,head=pn;
else{
ListNode* h =head;
while(h->next->val<pn->val)h=h->next;
pn->next=h->next,h->next=pn;
}
}
return head;
}
};
440 ms
思路二:归并排序
//把两个升序的链表合并为一个升序的链表
ListNode* mergeTwoUpLists(ListNode* p, ListNode* q) {
if(!p)return q;
if(!q)return p;
ListNode *head;
if(p->val < q->val)head=p,p=p->next;
else head=q,q=q->next;
ListNode *ans=head;
while(p && q)
{
if(p->val < q->val)ans->next=p,ans=p,p=p->next;
else ans->next=q,ans=q,q=q->next;
}
if(p)ans->next=p;
else ans->next=q;
return head;
}
class Solution {
public:
ListNode* sortList(ListNode* head,int len) {
if(len<2)return head;
int k=len/2;
ListNode* p=head;
for(int i=0;i<k-1;i++)p=p->next;
ListNode* p2=sortList(p->next,len-k);
p->next=NULL;
p=sortList(head,k);
return mergeTwoUpLists(p,p2);
}
ListNode* sortList(ListNode* head) {
int len=LinkGetLength(head);
return sortList(head,len);
}
};
132 ms
思路三:
直接调用我的ListSort函数模板:ACM模板(9)链表
POJ 2299 Ultra-QuickSort(归并排序)
题目:
Description
In this problem, you have to analyze a particular sorting algorithm. The algorithm processes a sequence of n distinct integers by swapping two adjacent sequence elements until the sequence is sorted in ascending order. For the input sequence 9 1 0 5 4 ,Ultra-QuickSort produces the output 0 1 4 5 9 .
Your task is to determine how many swap operations Ultra-QuickSort needs to perform in order to sort a given input sequence.
Input
The input contains several test cases. Every test case begins with a line that contains a single integer n < 500,000 -- the length of the input sequence. Each of the the following n lines contains a single integer 0 ≤ a[i] ≤ 999,999,999, the i-th input sequence element. Input is terminated by a sequence of length n = 0. This sequence must not be processed.
Output
For every input sequence, your program prints a single line containing an integer number op, the minimum number of swap operations necessary to sort the given input sequence.
Sample Input
5
9
1
0
5
4
3
1
2
3
0
Sample Output
6
0
题意很好理解,输入一个数列,输出它的逆序数。
我的原始代码(超时):
#include<iostream>
using namespace std;
int n;
int c[500005];
int num[500005];
int sum(int i)
{
int s = 0;
while (i)
{
s += c[i];
i -= (i&(-i));
}
return s;
}
void add(int i, int x)
{
while (i <= n)
{
c[i] += x;
i += (i&(-i));
}
}
int findmax()
{
int max = 1;
for (int j = 2; j <= n; j++)if (num[max] < num[j])max = j;
return max;
}
int main()
{
ios_base::sync_with_stdio(false);
long long s;
while (cin >> n)
{
if (n == 0)break;
for (int i = 1; i <= n; i++)
{
cin >> num[i];
c[i] = 0;
}
s = 0;
for (int i = 1; i <= n; i++)
{
int j = findmax();
num[j] = -1;
s += sum(j);
add(j, 1);
}
cout << s << endl;
}
return 0;
}
很明显它的时间是n*n,所以超时了。
但是好在思路差的不是很多,稍微改了下就对了。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
struct node
{
int num;
int index;
};
int n;
int c[500005];
node nod[500005];
bool cmp(node a, node b)
{
return a.num > b.num;
}
int sum(int i)
{
int s = 0;
while (i)
{
s += c[i];
i -= (i&(-i));
}
return s;
}
void add(int i, int x)
{
while (i <= n)
{
c[i] += x;
i += (i&(-i));
}
}
int main()
{
ios_base::sync_with_stdio(false);
long long s;
while (cin >> n)
{
if (n == 0)break;
for (int i = 1; i <= n; i++)
{
cin >> nod[i].num;
nod[i].index = i;
c[i] = 0;
}
s = 0;
sort(nod + 1, nod + 1 + n, cmp);
for (int i = 1; i <= n; i++)
{
int j = nod[i].index;
s += sum(j);
add(j, 1);
}
cout << s << endl;
}
return 0;
}
这个虽然过了,但是很慢,我猜想手写快速排序或者归并排序应该会比较快。
普通的归并排序,只需要加一句sum += mid - i + 1;就可以变成边排序边统计逆序数。
代码:
#include<iostream>
using namespace std;
int n;
long long sum;
int num[500005];
int copynum[500005];
void merge(int low, int high)
{
int mid = (low + high) / 2;
int i = low, j = mid + 1, k = low;
while (i <= mid && j <= high)
{
if (num[i] < num[j])copynum[k++] = num[i++];
else
{
copynum[k++] = num[j++];
sum += mid - i + 1;
}
}
while (i <= mid)copynum[k++] = num[i++];
while (j <= high)copynum[k++] = num[j++];
for (int i = low; i <= high; i++)num[i] = copynum[i];
}
void sort(int low, int high)
{
if (low == high)return;
int mid = (low + high) / 2;
sort(low, mid);
sort(mid + 1, high);
merge(low, high);
}
int main()
{
ios_base::sync_with_stdio(false);
while (cin >> n)
{
if (n == 0)break;
for (int i = 1; i <= n; i++)cin >> num[i];
sum = 0;
sort(1, n);
cout << sum << endl;
}
return 0;
}