排序
8.1 排序(Sort)基本的概念
一. 排序的定义
二. 排序的功能
三. 排序的分类
内排序——参加排序的数据量不大,以致于能够一次将参加排序的数据全部装入内存实现排序。
外排序——当参加排序的数据量很大,以致于不能够一次将参加排序的数据全部装入内存,排序过 程中需要不断地通过内存与外存之间的数据交换达到排序目的。
连续顺序表排序:改变记录的物理位置
链接顺序表排序:
四. 排序的性能
①时间性能——排序过程中元素之间的比较次数与元素的移动次数
(本章讨论各种排序方法的时间复杂度主要按照最差情况下所需要的比较次数来进行)
②空间性能——除了存放参加的元素之外,排序过程中所需要的其他辅助空间
③稳定性——对于值相同的两个元素,排序前后的先后次序不变,则称该方法为稳定性排序方法,否则成为非稳定性排序方法
(在所有可能的输入实例中,只要有一个实例使得该排序方法不满足稳定性要求,该排序方法就是非稳定的!)
趟(Pass)——将具有n个数据元素(关键字)的序列转换为一个按照值的大小从小到大排列的序列通常要经过若干“趟”
8.2 插入排序法
核心思想:第i趟排序将序列的第i+1个元素插入到一个大小为i、且已经按值有序的子序列(k(i-1),1 , k(i-1),2 , … , k(i-1),i)的合适位置,得到一个大小为i+1、且仍然按值有序的子序列(k i,1 , k i,2,… , k i,(i+1))。
k(i,j) 表示第i趟排序结束时,序列的第j个元素
1 <= i <= n-1;
1 <= j <= n;
插入排序法算法:
折半插入排序:稳**定,**时间复杂度O(n^2)
void insertSort(keytype k[] , int n)
{
int i , j;
keytype temp;
for(i = 1 ; i < n ; i++)//一共进行n-1趟排序,即从第二个元素开始,到最后一个元素,依次把这n-1个元素插入到前面中去
{
temp = k[i];//用temp保存k[j]的值
for(j = i-1 ; j >= 0 && temp < k[j] ; j--)
{
//当temp比k[j]小的时候,将temp继续往前比较,同时交换位置(即把插入点之后的数整体后移)
//当j == -1 或 temp >= k[j]的时候出循环,这时候,应该把k[j+1]赋值为temp
k[j+1] = k[j];
}
k[j+1] = temp;
}
}
折半插入排序法算法:
折半插入排序:稳**定,**时间复杂度O(n^2),空间代价O(1)
可以减少比较的次数,但是不能减少移动的次数
比直接插入算法明显减少了关键字之间比较的次数,因此速度比直接插入排序算法快,但记录移动的次数没有变,所以折半插入排序算法的时间复杂度仍然为O(n^2),与直接插入排序算法相同。
最佳情况:n-1次比较,0交换,O(n)
最差情况:比较和交换次数为O(n^2)
平均情况:O(n^2)
void insertBSort(keytype k[] , int n)
{
int i , j , low , high , mid;
keytype temp;
for(i = 1 ; i < n ; i++)
{
temp = k[i];
low = 0 ;
high = i - 1;
while( low <= high )//找到位置的标志:high < low
{
mid = (low + high)/2;
if(temp < key[mid])//如果temp要插入在mid之前
{
high = mid - 1;
}
else//如果temp要插入在mid之后
{
low = mid + 1;
}
}
for(j = i-1 ; j >= low ; j--)//插入点(low)后整体后移
{
k[j+1] = k[j];
}
key[low] = temp;//插入点为low
}
}
请写一非递归算法,该算法在长度为 n、且元素按值严格递增排列的顺序表A[1…n]中采用折半查找法查找值不大于k的最大元素,若表中存在这样的元素,则算法返回该元素在表中的位置,否则返回0。
int searchB(keytype a[ ],int n, keytype k)
{
int low=0, high=n-1, mid;
while(low<=high){
mid=(low+high)/2;
if(a[mid]==k)
return mid; /* 返回mid */
if(k>a[mid])
low=mid+1; /* 准备查找后半部分 */
else
high=mid–1; /* 准备查找前半部分 */
}
return high; /* 返回high */
}
8.3选择排序法
核心思想:第i趟排序从序列的后 n-i+1 个元素中选择一个值最小的元素,将其置于该 n-i+1个元素的最前面
选择排序算法:
选择排序:不稳定,时间复杂度:O(n^2)
选择排序算法的元素之间的比较次数与原始序列中元素的分布状态无关
不稳定是因为要”交换“,可能会破坏相对顺序,如给:5,8,5,2,9 排序时
比较次数:O(n^2)
交换次数:n-1
总时间代价:O(n^2)
void selectSort (keytype k[] , int n)
{
int i , j , d;
keytype temp;
for(i = 0 ; i < n - 1 ; i++)//共进行 n-1 趟排序
{
d = i;
for(j = i+1 ; j < n ; j++)//寻找值最小的元素并记录其位置
if(k[j] < k[d])
d = j;
if(d != i)//当(最小值元素)非(未排序元素 的 第一个元素)时
{
temp = k[d];
k[d] = k[i];
k[i] = temp;
}
}
}
void selectSort(int array[] , int n)//一个简洁的选择排序
{
int i , j , tmp;
for(i = 0 ; i < n ; i++)
{
for(j = i ; j < n ; j++)
{
if(array[i] > array[j])
{
tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
8.4 冒泡排序法
核心思想:第i趟排序对序列的前 n-i+1个元素从第一个元素开始依次进行如下操作:相邻的两个元素比较大小,若前者大于后者,则两个元素交换位置,否则不交换位置。(效果:该n-i+1个元素中最大值元素移动到该n-i+1个元素最后)
排序总趟数可以小于n-1:设置一标志flag,每一趟排序前置flag = 0,排序过程中出现元素交换动作,置 flag 为1。
冒泡排序方法比较适合于参加排序的序列的原始状态基本有序的情况
冒泡排序算法:
冒泡排序:稳定,时间复杂度一般O(n^2),最小O(n),空间代价O(1)
void bubbleSort(keytype k[] , int n)
{
int i , j , flag = 1;
keyttype temp;
for(i = n - 1 ; i > 0 && flag == 1 ; i--)
{//每一趟,比较i次(特别的,对第一趟,比较n-1次(一共n个元素))
flag = 0;//每趟排序前,flag置0
for(j = 0 ; j < i ; j++)//每一个k[j]和k[j+1]比较,每一趟比较i次
{
if(k[j] > k[j+1])
{
temp = k[j];
k[j] = k[j+1];
k[j+1] = temp;//交换两个元素的位置
flag = 1;//置flag为1
}
}
}
}
8.5 谢尔排序法
核心思想:
首先确定一个元素的间隔数gap
将参加排序的元素按照gap分隔成若干个子序列(即分别把那些位置相隔为gap的元素看作一个子序列),然后对各个子序列采用某一种排序方法进行排序;此后减小gap值,重复上述过程,直到gap < 1
一种减小gap的方法:
gap(1) = [n/2]
gap(i) = [gap(i-1)/2] i = 2 , 3 , …
shell排序:不稳定;时间复杂度:O(n·log2(n))与O(n2)之间,通常<O(n3/2);空间代价:O(1)
void shellSort(keytype k[], int n)
{
int i , j , flag , gap = n;
keytype temp;
while(gap > 1)//实际上最小的gap是1(就是这里的gap再/2)
{
gap = gap/2;
do{
flag = 0;
for(i = 0 ; i < n - gap ; i++)//每趟排序
{
//子序列内采用泡排序方法
j = i + gap;
if(k[i] > k[j])
{
temp = k[i];
k[i] = k[j];
k[j] = temp;
flag = 1;
}
}
}while(flag != 0);
}
}
void shellSort(int v[] , int n)//from K & R
{
int gap , i , j , temp;
for(gap = n/2 ; gap > 0 ; gap /= 2)
{
for(i = gap ; i < n ; i++)
{
for(j = i - gap ; j >= 0 && v[j] > v[j+gap] ; j-=gap)
{
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
}
}
void shellSort(keytype k[] , int n)
{
int i , j , gap = n;
keytype temp;
while(gap > 1)
{
gap = gap / 2;
for(i = gap ; i < n ; i++)//使用插入排序法实现子序列排序
{
temp = k[i];
for(j = i ; j >= gap && k[j - gap] > temp ; j -= gap)
{
k[j] = k[j - gap];
}
k[j] = temp;
}
}
}
8.6 堆排序法
一. 堆的定义
- n个元素的序列(k1 , k2 , … , kn),当且仅当满足ki >= k2i 且 ki >= k 2i+1 (i = 1 , 2 , 3 , … , [n/2]),称该序列为一个堆积(Heap),简称堆。
本课程的堆为大顶堆,将 >= 替换为 <= ,则是小顶堆。
- 堆是一颗完全二叉树,二叉树中任何一个分支结点的值都大于或者等于它的孩子结点的值,并且每一棵子树也满足堆特性
二. 排序的核心思想
第i趟排序将序列的前n-i+1个元素组成的子序列转换成一个堆积,然后将堆的第一个元素与堆的最后一个元素交换位置
三. 排序步骤
- 将原始序列转换为第一个堆
- 将堆的第一个元素与堆积的最后那个元素交换位置。(即“去掉”最大值元素)
- 将“去掉”最大值元素后剩下的元素组成的子序列重新转换一个新的堆
- 重复上述过程的第2至第3步n-1次
四. 调整子算法
功能:向下调整结点i的位置,使得其祖先节点值都比其大。
如果一棵树仅根节点i不满足堆条件,通过该函数可将其调整为一个堆
void adjust(keytype k[] , int i , int n)
{
int j;
keytype temp;
temp = k[i];
j = 2*i +1;
//K:序列
//i:被调整的二叉树的根的序号
//n:被调整的二叉树结点数目
while(j < n)
{
if(j + 1 < n && k[j] < k[j+1])
{
j++;
}
if(temp < k[j])
{
k[(j-1)/2] = k[j];
j = 2*j + 1;
}
else break;
}
k[(j-1)/2] = temp;
}
五. 建初始堆
从二叉树的最后那个分支结点(编号为i = [n/2 - 1])开始,依次将编号为i 的结点为根的二叉树转换为一个堆,每转换一棵子树,做一次i-1,重复上述过程,直到将i = 0 的结点为根的二叉树转换为堆
六. 堆排序算法
堆排序:不稳定,时间复杂度O(nlog2(n)),空间代价:O(1)
建初始堆积复杂度:<= O(n)
void heapSort(keytype k[] , int n)
{
int i;
keytype temp;
for(i = n/2-1 ; i >= 0 ; i--)//建立初始堆积
{
adjust(k , i , n);
}
for(i = n-1 ; i >= 1 ; i--)//具体排序,共(n-1)趟
{
temp = k[i];
k[i] = k[0];
k[0] = temp;
adjust(k , 0 , i);
}
}
8.7 二路归并排序法
一. 什么是二路归并?
将两个位置相邻、并且各自按值有序的子序列合并为一个按值有序的子序列的过程称为二路归并
合并算法:
功能:将两个位置相邻且按值有序的子序列合并为一个按值有序的序列
void merge(keytype x[] , keytype tmp[] , int left , int leftend , int rightend)
{
int i = left , j = leftend+1 , q = left;
while(i <= leftend && j < rightend)
{
if(x[i] <= x[j])
tmp[q++] = x[i++];
else
tmp[q++] = x[j++];
}
while(i <= leftend)//复制第一个子序列的剩余部分
{
tmp[q++] = x[i++];
}
while(j <= rightend)//复制第二个子序列的剩余部分
{
tmp[q++] = x[j++];
}
for(i = left ; i <= rightend ; i++)//将合并后内容复制回原数组
{
x[i] = tmp[i];
}
}
二. 核心思想
第i趟排序将序列的[ n/ (2^(i-1) ) ]个长度为2^(i-1)的按值有序的子序列依次两两合并为[n / (2^i) ]个长度为2^i的按值有序的子序列。
若n != 2^k ,则最后一趟在两个不等长的序列中进行,排序趟数为[log2(n)]
三. 归并排序算法
归并排序:稳定,时间复杂度O(n·log2(n)) ,空间复杂度O(n)
时间复杂度:不依赖于原始数据的输入情况,最大、最小及平均时间代价均为O(n·log2(n))
本质上它是一种分治算法
void mergeSort(keytype k[] , int n)
{
keytype *tmp;
tmp = (keytype*)malloc(sizeof(keytype)*n);
if(tmp != NULL)
{
mSort(k , tmp , 0 , n-1);
free(tmp);
}
else
{
printf("No space for tmp array!!!\n");
}
}
void mSort(keytype k[] , keytype tmp[] , int left , int right)
{
int center;
if(left < right)
{
center = (left + right)/2;
mSort(k , tmp , left , center);
mSort(k , tmp , center+1 , right);
merge(k , tmp , left , center , right);
}
}
8.8 快速排序法
一. 核心思想
从当前参加排序的元素中任选一个元素(通常称之为分界元素pivot)与当前参加排序的那些元素进行比较,凡是小于分界元素的元素都移到分界元素的前面,凡是大于分界元素的元素都移到分界元素的后面,分界元素将当前参加排序的元素分成前后两部分,而分界元素处在排序的最终位置。然后,分别对这两部分中大小大于1的部分重复上述过程,直到排序结束。
分界元素:划分元素、基准元素、枢轴、轴值、支点
可以选第一个或者最后一个、或位置居中的那个元素作为分界元素
递归过程
目前实践中已知最快的排序算法
每一次排序至少可以确定一个元素的最终位置!
二. 算法步骤
算法中用到的变量:
left:当前参加排序的那些元素的第一个元素在序列中的位置,初始值为0.
right:当前参加排序的那些元素的最后那个元素在序列中的位置,初始值为n-1.
i , j :两个位置变量,初始值分别为left与right+1
步骤:
-
反复执行动作i +1→i,直到K[left] <= K[i] 或者i = right
反复执行动作j - 1→j,直到K[left] >= K[j] 或者j = left
-
若i < j,则K[i]与K[j]交换位置,转到第一步
-
若i >= j,则K[left]与K[j]交换位置,到此,分界元素K[left]的最终位置已经确定(j),然后对被K[left]分成的两部分中个数大于1的部分重复上述过程,直到排序结束
分界元素K[S]!
快速排序算法(递归):
不稳定,时间复杂度O(n·log2(n))
最差情况: | 时间代价:O(n^2) 空间代价:O(n) |
---|---|
最佳情况: | 时间代价:O(n·log2(n)) 空间代价:O(log2(n)) |
平均情况: | 时间代价:O(n·log2(n)) 空间代价:O(log2(n)) |
//主算法:
void quickSort(keytype k[] , int n)
{
quick(K , 0 , n-1)
}
void quick(keytype k[] , int left , int right)
{
int i , j;
keytype pivot;
if(left < right)
{
i = left ; j = right + 1;
pivot - k[left];
while(1)
{
while(k[++i] < pivot && i != right){}
while(k[--j] < pivot && j != left){}
if(i < j)
{
swap(&k[i] , &k[j])//交换K[i]与K[j]的内容
}
else
{
break;
}
swap(&k[left] , &k[j]);//交换K[s]与K[j]的内容
quick(k , left , j-1);//对前一部分排序
quick(k , j+1 , right);//对后一部分排序
}
}
}
void quickSort(keytype k[] , int n)//主算法
{
qsort(k , 0 , n-1);
}
void qsort(keytype v[] , int left , int right)//from K & R
{
int i , last;
if(left >= right)
return;
swap(v , left , (left + right)/2);//move partition elem to v[0]
last = left;
for(i = left+1 , i <= right ; i++)//partition
{
if(v[i] < v[left])
{
swap(v , ++last , i);
}
swap(v , left , last)//restore partition elem
qsort(v , left , last);
qsort(v , last+1 , right);
}
}//本质上它是一种分治算法
void swap(keytype v[] , int i , int j)
{
keytype tmp;
tmp = v[i];
v[i] = v[j];
v[j] = tmp;
}
关于快速排序:
在C、C++、Java等许多语言的开发库中均提供了快速排序算法的实现,在实际应用中可直接使用。如在C语言标准库中qsort函数原型如下:
void qsort(void *base, size_t n, size_t size, int (*cmp)(const void *, const void *))
#include <stdlib.h>
#include <stdio.h>
int main()
{
int i, a[ ] = { 23 , 12, 56, 5, 90, 67, 34, 87, 19, 2 };
qsort( a, 10, sizeof(int), intcmp);
for(i=0; i<10; i++)
printf(“%d ”, a[i]);
return 0;
}
int intcmp( int *s, int *t)//compar: 比较两个数组元素的比较函数。本比较函数的第一个参数值小于、等于、大于第二参数值时,本比较函数的返回值应分别小于、等于、大于零。
{
if(*s <*t) return -1;
else if( *s>*t) return 1;
else return 0;
}
排序算法总结
从算法性质来看:
简单算法:冒泡、选择、插入
改进算法:谢尔、堆、归并、快速
从时间复杂度来看:
平均情况:后3种改进算法 > 谢尔 (远)> 简单算法
最好情况:冒泡和插入排序要更好一些
最坏情况:堆和归并排序要好于快速排序及简单排序
从空间复杂度来看:归并排序有额外空间要求,快速排序也有相应空间要求,堆排序则基本没有。
从稳定性来看:除了简单排序,归并排序不仅速度快,而且还稳定
桶排序法(计数排序)
一. 核心思想
假设a1 , a2 , … , an由小于M的正整数组成,桶排序的基本原理是使用一个大小为M的数组C(初始化为0,称为桶bucket),当处理ai时,使C[ai]增1.最后遍历数组C输出排序后的表
基本思想是由E.J.Issac R.C.Singleton提出来。桶排序并不是比较排序,不受到O(n·logn)下限的影响,是最简单、最快的排序,时间复杂度为O(M+N)
void bucketSort(int k[] , int n)
{
int C[M] = {0} , i , j;
for(i = 0 ; i < n ; i ++)
{
C[K[i]]++;
}
for(i = 0 , j = 0 ; i < M ; i++)
{
if(C[i])
K[j++] = i;
}
}