一、定义
排序就是重新排列表中的元素,使表中的元素满足按关键字有序的过程,各类排序算法性能如下:
排序算法 | 最好情况 | 最坏情况 | 平均情况 | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|---|---|
插入排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 | 简单 |
希尔排序 | O ( n 1.3 ) O(n^{1.3}) O(n1.3) | O ( 1 ) O(1) O(1) | 不稳定 | 复杂 | ||
冒泡排序 | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 | 简单 |
快速排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n 2 ) O(n^2) O(n2) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( log 2 n ) O(\log_2n) O(log2n) | 不稳定 | 复杂 |
选择排序 | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 | 简单 |
堆排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( 1 ) O(1) O(1) | 不稳定 | 复杂 |
归并排序 | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n log 2 n ) O(n\log_2n) O(nlog2n) | O ( n ) O(n) O(n) | 稳定 | 复杂 |
基数排序 | O [ d ( n + r ) ] O[d(n+r)] O[d(n+r)] | O [ d ( n + r ) ] O[d(n+r)] O[d(n+r)] | O [ d ( n + r ) ] O[d(n+r)] O[d(n+r)] | O ( r ) O(r) O(r) | 稳定 | 复杂 |
二、插入排序
插入排序的思想是每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
1. 直接插入排序
顾名思义,就是每次取一个数据往有序表中插入即可,通常用一个变量记录新加入的元素,但也可以将数组中首位当作哨兵来记录新加入的元素。
直接插入排序算法适用于基本有序的表,性能可接近 O ( n ) O(n) O(n)。
// 数组从 0 开始
void InsertSort(ElemType A[], int n)
{
for(int i = 1; i < n; ++i)
if(A[i] < A[i - 1])
{
ElemType temp = A[i];
int j;
for(j = i - 1; temp < A[j] && j >= 0; --j)
A[j + 1] = A[j];
A[j + 1] = temp;
}
}
2. 折半插入排序
利用二分查找的思想,将直接插入排序中查找新元素位置的操作优化为二分查找,其余操作不变。
折半插入排序适用于基本有序的表,性能可接近 O ( n ) O(n) O(n)。
// 数组从 0 开始
void BinaryInsertSort(ElemType A[], int n)
{
int low,high,mid;
for(int i = 1; i < n; ++i)
{
ElemType temp = A[i];
low = 0;
high = i - 1;
while(low <= high)
{
mid = (low + high) / 2;
if(A[mid] > temp)
high = mid - 1;
else
low = mid + 1;
}
for(int j = i - 1; j >= low; --j)
A[j + 1] = A[j];
A[low] = temp;
}
}
3. 希尔排序
希尔排序的思想是先将待排序表进行分割,按一定增量将相隔该增量的元素组成一个新表,然后对各个子表分别进行插入排序,按此操作指导所有元素汇总到一个表中为止。
简单来说,希尔排序就是将表分割后多次进行插入排序。
// 数组从 1 开始
void ShellSort(ElemType A[], int n)
{
for(int dk = n / 2; dk >= 1; dk =dk/2)
for(int i = dk + 1; i <= n; ++i)
if(A[i] < A[i - dk])
{
A[0] = A[i];
int j;
for(j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
A[j + dk] = A[j];
A[j + dk] = A[0];
}
}
二、交换排序
交换排序的思想是通过对两个不同元素比较大小,然后进行交换而逐渐得到有序表。
1. 冒泡排序
冒泡排序的思想是从后往前两两比较相邻元素的值,若为逆序则交换它们,直到序列比较完。经过这样一趟处理后可以将最小值交换到待排序列的首位。然后我们再用同样的方式处理子序列,最终可得到完整有序的表。
// 数组从 0 开始
void BubbleSort(ElemType A[],int n)
{
for(int i=0; i<n-1; ++i)
{
bool flag=false;
for(int j=n-1; j>i; --j)
if(A[j]<A[j-1])
{
swap(A[j],A[j-1]);
flag=true;
}
if(!flag)
return;
}
}
2. 快速排序
快速排序的思想是基于分治法的,选取任意元素为枢轴,通过一趟排序将待排序列表划分为独立的两部分,再递归的对两个子表进行快速排序,直到最终得到有序表。
一趟排序的过程是从头指针和尾指针开始交替搜索,然后进行交换操作,直到两个指针相遇,此时便找到了枢轴的最终位置。
// 数组任意位置开始
int Partition(ElemType A[],int low,int high)
{
ElemType pivot=A[low];
while(low<high)
{
while(low<high&&A[high]>=pivot)
--high;
A[low]=A[high];
while(low<high&&A[low]<=pivot)
++low;
A[high]=A[low];
}
A[low]=pivot;
return low;
}
void QuickSort(ElemType A[],int low,int high)
{
if(low<high)
{
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
三、选择排序
选择排序的思想是每次从待排序序列中选取最小值放入有序表中,最终得到有序表。
1. 简单选择排序
顾名思义,完美践行了上述定义。
// 数组从 0 开始
void SelectSort(ElemType A[],int n)
{
for(int i=0; i<n-1; ++i)
{
int min=i;
for(int j=i+1; j<n; ++j)
if(A[j]<A[min])
min=j;
if(min!=i)
swap(A[i],A[min]);
}
}
2. 堆排序
首先来了解堆的定义,堆分大根堆与小根堆,我们以大根堆为例,假设现在有一组序列,我们将其按照层次遍历的方式展开成二叉树的结构,若这棵二叉树严格满足父结点大于子结点,那么它就是一个大顶堆。小顶堆则相反,严格满足父结点小于子结点。
由于二叉树的性质,我们可以发现子结点在序列中所对应的下标,就是父结点在序列所对应的下标的两倍或再加 1 。
现在再来聊聊如何构造一个大顶堆。
既然我们知道了堆的定义,那么只需将所有子结点与它的父结点作比较,如果它更大,就将它与父结点交换位置,然后再递归的进行相同的操作,直到小于它的父结点为止。这个步骤叫做下坠,当所有结点严格满足大顶堆定义时,就构造了一个大顶堆。
堆的插入操作就是将新元素放在最后面,然后对它进行下坠操作即可。
堆的删除操作是将待删除结点与末尾结点交换,然后删除它,此时的序列可能不满足大顶堆的定义,因此需要对所有子结点进行下坠操作。
由于堆顶元素是我们要找的最小元素,因此堆排序就是每次对剩余序列构造小顶堆,然后将其取出放在有序序列中,这样便最终得到有序序列。
// 数组从 0 开始
void HeadAdjust(ElemType A[],int k,int len)
{
A[0]=A[k];
for(int i=2*k; i<=len; i*=2)
{
if(i<len&&A[i]<A[i+1])
i++;
if(A[0]>=A[i])
break;
else
{
A[k]=A[i];
k=i;
}
}
A[k]=A[0];
}
void BuildMaxHeap(ElemType A[],int len)
{
for(int i=len/2; i>0; i--)
HeadAdjust(A,i,len);
}
void HeapSort(ElemType A[],int len)
{
BuildMaxHeap(A,len);
for(int i=len; i>1; i--)
{
swap(A[i],A[1]);
HeadAdjust(A,1,i-1);
}
}
四、归并排序
归并排序的思想是对有序表进行合并来得到更大的有序表,合并时只需每次比较两个(或多个)有序表中的首个元素大小,选择最小元素放入新的有序表即可。
归并排序一开始将每个元素看作一个单独的有序序列,然后两两进行合并,递归操作直到最终合并出一个有序表。
运算器对大量数据进行排序时,由于内存空间有限,因此常用到多路归并排序,由于硬盘 IO 操作很浪费时间,因此还可以使用败者树来进行优化。
// 数组从任意位置开始
ElemType *B=(ElemType*)malloc((10)*sizeof(ElemType));
void Merge(ElemType A[],int low,int mid,int high)
{
for(int k=low; k<=high; ++k)
B[k]=A[k];
int i,j,k;
for(i=low,j=mid+1,k=i; i<=mid&&j<=high; k++)
if(B[i]<=B[j])
A[k]=B[i++];
else
A[k]=B[j++];
while(i<=mid)
A[k++]=B[i++];
while(j<=high)
A[k++]=B[j++];
}
void MergeSort(ElemType A[],int low,int high)
{
if(low<high)
{
int mid=(low+high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high);
}
}
五、基数排序
基数排序是一个剑走偏锋的算法,它不去考虑数据整体的大小,而是将其按不同位进行排序,最终来得到一个有序序列。举个栗子,对于百位数而言,首先将所有数按个位大小进行排序,然后按十位大小进行排序,最后按百位大小进行排序,最终便可以得到一个有序序列。
// 数组从 0 开始
void RadixSort(ElemType A[], int n)
{
vector<ElemType> flag[10];
for(int i = 0; i < n; i++)
flag[A[i] % 10].push_back(A[i]);
int start = 0;
for(int i = 0; i < 10; ++i)
for(int j = 0; j < flag[i].size(); ++j)
A[start++] = flag[i][j];
for(int i = 0; i < 10; ++i)
flag[i].clear();
for(int i = 0; i < n; i++)
flag[A[i] / 10 % 10].push_back(A[i]);
start = 0;
for(int i = 0; i < 10; ++i)
for(int j = 0; j < flag[i].size(); ++j)
A[start++] = flag[i][j];
for(int i = 0; i < 10; ++i)
flag[i].clear();
for(int i = 0; i < n; i++)
flag[A[i] / 100].push_back(A[i]);
start = 0;
for(int i = 0; i < 10; ++i)
for(int j = 0; j < flag[i].size(); ++j)
A[start++] = flag[i][j];
}
六、计数排序
计数排序常用于含有大量重复数的情况,并且排序速度快于任何已知算法。计数排序的思想很简单,就是从最小数开始数,将每个数出现的次数标记出来,然后重新排列即可得到有序表。举个栗子,假设此时要对数学成绩进行排序,满分位 100 分,我们可以从 0 数到 100 ,然后统计出每个数出现的次数,最后即可得到有序表。
// 数组从 0 开始,inf表示最大值
void CountingSort(ElemType A[], int n)
{
ElemType minimum = inf, maximum = 0 - inf;
for(int i = 0; i < n; ++i)
{
minimum = min(A[i], minimum);
maximum = max(A[i], maximum);
}
ElemType flag[maximum + 1];
for(int i = 0; i <= maximum; i++)
flag[i] = 0;
for(int i = 0; i < n; ++i)
flag[A[i]]++;
int start = 0;
for(int i = minimum; i <= maximum; ++i)
for(int j = 0; j < flag[i]; ++j)
A[start++] = i;
}
七、桶排序
桶排序的思想跟基数排序的思想是一致的,只不过由于计数排序只能用于整型序列,因此桶排序对它的思想进行了扩充。
桶排序要求将整个数据序列分成多个组,每个组代表一个桶,在每个桶内采用一定的排序算法进行排序,最终构成一个有序表。
在桶排序中,每个桶分到的数据个数越均等,桶的数目越多,则该算法效率越高。
//链表结点描述
typedef struct Node
{
double key;
struct Node * next;
} Node;
// hash算法为元素值乘 10 的整数部分作为桶
void BucketSort(double *a, int n)
{
Node head[10] = {NULL};
Node *p;
Node *q;
Node *node;
for(int i = 0; i < n; i++)
{
node = (Node*)malloc(sizeof(Node));
node->key = a[i];
node->next = NULL;
p = q = head[(int)(a[i]*10)].next;
if(p == NULL)
{
head[(int)(a[i]*10)].next = node;
continue;
}
while(p)
{
if(node->key < p->key)
break;
q = p;
p = p->next;
}
if(p == NULL)
q->next = node;
else
{
node->next = p;
q->next = node;
}
}
for(int i = 0, j = 0; i < 10; i++)
{
p = head[i].next;
while(p)
{
a[j++] = p->key;
p = p->next;
}
}
}