排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是大文件的排序,即待排序的数据记录存储在外部存储器上,在排序过程中需要进行多次的内、外存之间的交换。
注:当n较大时应采用时间复杂度为O(nlogn)的排序算法:快速排序、堆排序或归并排序。
排序算法的稳定性:
定义:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。通俗地说稳定性就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
稳定性的重要性:在基数排序的体现。(转载自:九大排序算法再总结)
1交换排序
1.1冒泡排序
思想:
相邻元素比较、交换,从第一对相邻元素到最后一对,每次将得到的最大值排在最后面。除了最后一个元素,重复以上步骤,直到只有一个元素为止。
图示:
实现:
void bubble_sort(int a[], int n)
{
int i, j, temp;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - 1 -i; j++)
{
if (a[j] > a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
return ;
}
/*冒泡排序时间复杂度最佳情况为O(n)的实现*/
void bubble_sort(int a[], int n)
{
int i, j, temp;
int is_sorted;
for (i = 0; i < n - 1; i++)
{
is_sorted = 1;
for (j = 0; j < n - 1 -i; j++)
{
if (a[j] > a[j+1])
{
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
is_sorted = 0;
}
}
/*当序列已经是有序时,使得时间复杂度为O(n)*/
if (is_sorted == 1)
{
return ;
}
}
return ;
}
1.2快速排序
思想:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
图示:
实现:
具体做法:
【1】设置两个变量low和high,其初值分别为low=0,high=n-1。设置枢轴关键字key为第一个元素的值,即key=a[0];
【2】分区操作。
从j =high开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值a[j],将a[j]和a[i]互换。
从i=low开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的值a[i],将a[i]和a[j]互换。
重复上述两个步骤,直到i==j
【3】分别递归处理新的子分区的分区操作,直到low>=high
/*版本1:轴枢关键字key是第一个元素a[0]*/
void quick_sort(int a[], int low, int high)
{
int i = low;
int j = high;
int key = a[low];
/*子分区的排序已经完成——递归结束条件*/
if (low >= high)
{
return;
}
/*分区*/
while (i < j)
{
while (j > i && a[j] >= key)
{
j--;
}
a[i] = a[j];/*将第一个小于key的a[j]与a[i]交换*/
while (i < j && a[i] <= key)
{
i++;
}
a[j] = a[i];/*将第一个大于key的a[i]与a[j]交换*/
}
a[i] = key;/*完成一次分区后,将中间数key回归*/
/*递归操作*/
quick_sort(a, low, i -1);
quick_sort(a, i + 1, high);
}
/*版本2:轴枢关键字key是最后一个元素a[n-1]*/
void quick_sort(int a[], int low, int high)
{
int i = low;
int j = high;
int key = a[high];
int temp;
/*子分区的排序已经完成*/
if (low >= high)
{
return;
}
/*分区*/
while (i < j)
{
while (i < j && a[i] <= key)
{
i++;
}
temp = a[i]; /*a[i]与轴枢a[j]交换,使得轴枢在a[i]上*/
a[i] = a[j];
a[j] = temp;
while (j > i && a[j] >= key)
{
j--;
}
temp = a[i]; /*a[j]与轴枢a[i]交换,使得轴枢在a[j]上*/
a[i] = a[j];
a[j] = temp;
}
quick_sort(a, low, i -1);
quick_sort(a, i + 1, high);
}
/*快速排序的非递归实现*/
#define N 100
typedef struct node
{
int low;
int high;
}node_t;
/*顺序栈结构体*/
typedef struct stack
{
node_t s[N];//栈
int top;//栈顶指针,top为0表示空栈
}seq_stack_t;
/*初始化一个空栈*/
void init_stack(seq_stack_t *stack)
{
memset(stack->s, 0, N*sizeof(node_t));
stack->top = 0;
return ;
}
/*出栈*/
void pop(seq_stack_t *stack, node_t *node)
{
if (stack->top > 0)
{
stack->top--;
node->low = stack->s[stack->top].low;
node->high = stack->s[stack->top].high;
}
return ;
}
/*进栈*/
void push(seq_stack_t *stack, node_t *node)
{
stack->s[stack->top].low = node->low;
stack->s[stack->top].high = node->high;
stack->top++;
return ;
}
/*判断栈空*/
int empty(seq_stack_t *stack)
{
return (stack->top == 0);
}
/*分区操作*/
int partition(int a[], int i, int j)
{
int key = a[i];
while (i < j)
{
while (j > i && a[j] >= key)
j--;
a[i] = a[j];
while (i < j && a[i] <= key)
i++;
a[j] = a[i];
}
a[i] = key;
return i;
}
/*利用栈实现快速排序的非递归版本*/
void quick_sort(int a[], int low, int high)
{
node_t node;
seq_stack_t stack;
/*初始化一个空栈*/
init_stack(&stack);
/*第一个节点进栈*/
node.low = low;
node.high = high;
push(&stack, &node);
while (!empty(&stack))
{
node_t temp;
/*出栈*/
pop(&stack, &temp);
/*分区操作*/
int pivot = partition(a, temp.low, temp.high);
/*左分区进栈*/
if (temp.low < pivot - 1)
{
node_t node1;
node1.low = temp.low;
node1.high = pivot - 1;
push(&stack, &node1);
}
/*右分区进栈*/
if (temp.high > pivot + 1)
{
node_t node2;
node2.low = pivot + 1;
node2.high = temp.high;
push(&stack, &node2);
}
}
return ;
}
总结:
【1】平均时间复杂度的计算:
每次分成两个子区,分的次数为logn,即2^x=n,x为logn
每个分区的处理需要计算n次,故时间复杂度为O(nlogn)
【2】最坏时间复杂度的计算:
每次分成的两个子区为1和n-1,每个分区的处理需要计算n次,故而时间复杂度为O(n^2)。这种情况出现在序列是倒序的时候,每次分区取的中间关键字要么最大值要么最小值。使得两个子分区为1和n-1。为避免这种情况的发生,可采用三数取中的方法,即从a[0],a[n/2],a[n-1]中取一个中间值作为key,再将其与第一个位置或最后一个位置的元素交换,这样就能使用我们熟悉的key在首位置或尾位置的分区方法。这个时候,出现最坏情况的概率为(1/2^n)。
2选择排序
2.1直接选择排序
思想:
固定位置,找元素。与冒泡排序相比,选择排序并不急着调换位置。先是从a[0]开始逐一查找比较,获得最小的值后再与第一位置上的元素调换位置。接着,再找剩余元素的最小值放置在第二位置上。以此类推。
图示:
实现:
void select_sort(int a[], int n)
{
int i, j;
int min, idx;
for (i = 0; i < n; i++)
{
min = a[i];
idx = i;
for (j = i+1; j < n; j++)
{
if (a[j] < min)
{
min = a[j];/*记录最小的值及其坐标*/
idx = j;
}
}
/*仅交换一次*/
a[idx] = a[i];
a[i] = min;
}
return ;
}
2.2堆排序
定义:
二叉堆是完全二叉树或近似完全二叉树。二叉树满足2个特性:
【1】父结点的键值总是大于(或小于)等于任何一个子节点的键值。
【2】每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)
最大堆:父结点的键值总是大于或等于任何一个子结点的键值。
最小堆:父结点的键值总是小于或等于任何一个子结点的键值。
堆的存储:
一般都用数组来表示堆,i结点的左右子结点分别是2*i+1和2*i+2,父结点为(i-1)/2.如第0个结点的左右子结点分别为1和2.
数组int a[10] = { 2,9,8,4,5,1,3,7,10,6},与其表示的堆如下所示:
堆的操作:插入与删除
堆调整——把一个无序序列建成一个堆,即将数组堆化
参考堆的存储的图示,数组int a[10] = { 2,9,8,4,5,1,3,7,10,6},与其所示的堆。
其中对叶子结点来说,它们已经是一个合法的堆了,即1,3,7,10,6.现在所需要调整的元素是a[4],a[3],a[2],a[1]和a[0]。调整后,二叉堆的根结点为最大值(即最大堆)或最小值(即最小堆)。即只需调整数组的第n/2-1个元素至第0个元素。
堆排序:
最小堆中,若输出其堆顶值之后,使得剩余n-1个元素的序列又重建成一个堆,则得到n个元素的次小值。如此反复执行,最终得到一个有序序列,该过程称为堆排序。
图示:
实现:
/*堆调整,将数组中的第i个结点调整为最小值*/
void heap_adjust(int a[], int i, int n)
{
int j, temp;
temp = a[i]; /*待调整的结点*/
j = 2*i+1; /*左子结点*/
while (j < n)
{
/*选择左右子结点中较小的结点*/
if (j+1 < n && a[j+1] < a[j])
j++;
/*结点比左右子结点都小,则不需要调整*/
if (a[j] >= temp)
break;
/*存在比结点小的值,则该值上调,并继续比较该子结点与其左右子结点的大小*/
a[i] = a[j];
i = j;
j = 2*i+1;
}
/*将原先待调整的结点的值替换到已被上调的子结点的位置上*/
a[i] = temp;
}
void heap_sort(int a[], int n)
{
int i, temp;
/*建立最小堆*/
for (i= n/2 - 1; i >= 0; i--)
{
heap_adjust(a, i, n-1);
}
/*排序*/
for (i = n -1; i > 0; i--)
{
/*将最小堆中的最小值a[0],交换到最后的位置*/
temp = a[i];
a[i] = a[0];
a[0] = temp;
/*重新调整余下的元素的最小值到a[0]上*/
heap_adjust(a, 0, i);
}
}
3插入排序
3.1直接插入排序
思想:
固定元素,找位置。将一个记录插入到已排好序的有序表中,从而得到一个新的、记录增1的有序表。即通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
图示:
实现:
void insert_sort(int a[], int n)
{
int i, j, temp;
for (i = 1; i < n; i++)
{
if (a[i] < a[i-1])/*待排序的元素a[i]比已排序的序列的最后一个较大则不需要插入*/
{
temp = a[i];
for (j = i - 1; j>=0 && a[j]>temp; j--)
{
a[j+1] = a[j];/*从后向前扫描并向后移动*/
}
a[j+1] = temp;/*将待排序的元素temp插入到对应的位置*/
}
}
return ;
}
3.2希尔排序
思想:
希尔排序的实质是分组插入排序,又称“缩小增量排序”。先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
图示:
实现:
void shell_sort(int a[], int n)
{
int i, j, k;
int temp;
int delta;
for (delta = n/2; delta > 0; delta /= 2)//delta为增量或步长
{
/*分组插入排序*/
for (i = 0; i < delta; i++)
{
for (j = i + delta; j < n; j += delta)
{
if (a[j] < a[j - delta])
{
temp = a[j];
for (k = j - delta; k >= 0 && a[k] > temp; k -= delta)
{
a[k+delta] = a[k];
}
a[k+delta] = temp;
}
}
}
}
return ;
}
4归并排序
思想:
归并是将两个或两个以上的有序表组合成一个新的有序表。归并排序是建立在归并操作上的一种稳定的排序算法,该算法采用分治法。具体操作如下:
分割:将长度为n的序列分成两个长度为n/2的子序列。
递归:对两个子序列分别采用归并排序。
合并:将两个排好序的子序列合并成一个最终的排序序列。
图示:
实现:
void merge(int a[], int first, int mid, int last, int temp[])
{
int i = first, m = mid;
int j = mid+1, n = last;
int k = 0;
while (i <= m && j <= n)
{
temp[k++] = ((a[i] <= a[j]) ? a[i++]: a[j++]);
}
while (i <= m)
{
temp[k++] = a[i++];
}
while (j <= n)
{
temp[k++] = a[j++];
}
for (i = 0; i < k; i++)
{
a[first+i] = temp[i];
}
return ;
}
void merge_sort(int a[], int first, int last, int temp[])
{
if (first < last)
{
/*分割*/
int mid = (first+last)/2;
/*递归*/
merge_sort(a, first, mid, temp);
merge_sort(a, mid+1, last, temp);
/*合并*/
merge(a, first, mid, last, temp);
}
return ;
}
5基数排序
思想:
交换排序、选择排序、插入排序、归并排序等都是通过关键字间的比较和移动记录着两种操作来实现,而基数排序不需要进行记录关键字间的比较,它是通过分配和收集过程来实现排序。
具体过程:
如待排序序列为int a[10] = {73, 22, 93, 43, 55, 14, 28, 65, 39, 81}
【1】首先,根据个位数的数值,在遍历数据时将它们分配到编号为0到9的桶中。得到分配操作结果如下:
0 |
|
|
|
|
1 | 81 |
|
|
|
2 | 22 |
|
|
|
3 | 73 | 93 | 43 |
|
4 | 14 |
|
|
|
5 | 55 | 65 |
|
|
6 |
|
|
|
|
7 |
|
|
|
|
8 | 28 |
|
|
|
9 | 39 |
|
|
|
分配后,将所有桶中的数据按照桶号由小到大依次重新收集起来,得到如下依然无序的序列:
81 22 73 93 43 14 55 65 28 39
【2】接着,根据十位数上的数值再次进行分配操作,得到分配结果如下:
0 |
|
|
|
|
1 | 14 |
|
|
|
2 | 22 | 28 |
|
|
3 | 39 |
|
|
|
4 | 43 |
|
|
|
5 | 55 |
|
|
|
6 | 65 |
|
|
|
7 | 73 |
|
|
|
8 | 81 |
|
|
|
9 | 93 |
|
|
|
分配后,再次收集,得到如下序列:
14 22 28 39 43 55 65 73 81 93
【3】总结,上述是从数值的最低位到最高位进行的分配收集操作,称为最低位优先法(LSD),适用于位数少的序列;若从数值的最高位到最低位进行的分配收集操作,则称为最高位优先法(MSD),适用于位数多的序列。
实现:
以最高位优先法为例,
1先根据最高位关键字K1排序,得到若干对象组,对象组中每个对象都有相同的关键字K1
2再分别对每组中的对象根据关键字K2进行排序,按K2值的不同,再分成若干个更小的子对象组,每个子对象组中具有相同的K1和K2。
3以此重复,直到对关键字Kd完成排序为止。(d为代表长度)
4最后,把所有子对象组中的对象依次连接起来,就得到一个有序的对象序列。
/*获取num第pos位的数值*/
int getpos(int num, int pos)
{
int i, temp = 1;
for (i = 0; i < pos -1; i++)
{
temp *= 10;
}
return (num/temp)%10;
}
void msd_radix_sort(int a[], int bgn, int end, int pos)
{
int i,j;
int left, right;
const int radix = 10;/*桶的个数*/
int count[10];
for (i = 0; i < radix; i++)
{
count[i] = 0;
}
int *bucket = (int *)malloc((end-bgn+1)*sizeof(int));
/*统计各个桶的数据个数*/
for (i = bgn; i <= end; i++)
{
count[getpos(a[i], pos)]++;
}
/*计算桶的边界索引,count[i]为第i个桶的右边界索引*/
for (i = 1; i < radix; i++)
{
count[i] += count[i-1];
}
/*分配操作*/
for (i = end; i >= bgn; i--)
{
j = getpos(a[i], pos);
bucket[count[j]-1] = a[i];
--count[j];
}
/*收集操作*/
for (i = bgn, j = 0; i <= end; i++, j++)
{
a[i] = bucket[j];
}
free(bucket);
for (i = 1; i < radix; i++)
{
left = bgn + count[i-1]; /*第i个桶的左边界*/
right = bgn + count[i] - 1; /*第i个桶的右边界*/
if (left < right && pos > 1)
{
msd_radix_sort(a, left, right, pos - 1);
}
}
}
【2】序列n比较大的时候,快速排序、堆排序和归并排序较适合,为O(nlogn);且当序列基本无序的时候,快速排序更适合。