排序基本概念
排序,就是整理文件中的记录,使之按关键字递增(或递减)次序排列起来。
被排序的对象可以是文件或数据项。
排序的依据就是标识每个对象的关键字。对关键字进行排序,就相当于对关键字对应的文件进行排序。(有点像数据库SQL中的主键。)
排序的稳定性
如果序列中有相同项,即有相同的关键字。那么就涉及到排序的稳定性的问题。
排序后,相同值的项之间的相对次序没有发生变化,那么这个排序就是稳定的。
如果,排序后相同值的项之间的相对次序发生了变化,则这种排序方法是不稳定的。
不稳定的排序:
选择排序,希尔排序,堆排序,快速排序;
稳定的排序:
冒泡排序,双向冒泡排序,插入排序,二叉树排序等。
排序的分类
排序算法都要有两个基本的操作:
(1)比较项值的大小
(2)改变指向项的指针或移动项值本身
根据是否涉及数据的内、外存交换:
如果在排序过程中,整个文件都放在内存中处理,排序时不涉及数据的内、外存交换,则称之为内部排序。
否则,排序中涉及到数据的内外存交换,称之为外部排序。
排序的性能评价
标准:
1.执行时间和所需的辅助空间
2.算法本身的复杂度。
空间复杂度:
若算法所需的辅助空间并不依赖于问题的规模n,即辅助空间是O(1),则称之为就地排序。否则,非就地排序的辅助空间一般为O(n)。
时间复杂度:
1.平方阶排序O(n2):一般为简单排序,如插入排序,折半排序,选择排序和冒泡排序。
2.线性对数排序O(nlogn)排序:如快速,归并,堆排序。
3.O(n1+ε)阶排序:ε是介于0和1之间的常数,即0<ε<1,如希尔排序。
4.线性阶排序O(n):如桶,箱,和基数排序。
简单排序中插入排序最好,快速排序最快。
若n较小(n<50),可以采用插入排序和选择排序。
若n较大,则应采用时间复杂度为O(nlogn)的排序方法:快速排序,堆排序,归并排序。
平均角度而言,哈希表获取任意一个指定值是最快的。->查找用哈希。或是链表。
因为哈希表需要的额外存储空间多。付出了高额外空间的代价,理应获得高性能。
哈希函数,不管怎么样,都是输入键值对,输出桶编号。
二分查找法:
已知一个有序序列,找出指定元素的方法。
1.定义三个指针:low, mid, high,分别指向元素所在范围的下界、中间、上界。Mid=(high+low)/2
2.让元素与mid比较,若相等,返回mid;
若关键字小于mid对应的数,则high=mid-1,继续递归循环;
若关键字大于mid对应的数,则low=mid+1,继续递归循环;
3.直到找到所在位置。
冒泡法
冒泡排序和快速排序都是属于交换排序:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。
public static voidbubble(int[] a){
boolean exchange;//交换标志。
for(inti=0 ;i<a.length-1; i++){
exchange= false;
for(intj=0; j<a.length-1-i; j++){
if(a[j]>a[j+1]){
inttemp= a[j];
a[j]=a[j+1];
a[j+1]=temp;
exchange = true;//发生了交换,故将交换标志置为true
}
}
if(!exchange){//本次排序未发生交换,提前终止算法。
return;
}
}
for(intk=0;k<a.length; k++){
System.out.print(a[k]+"");
}
}
选择排序
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,放到排序序列的末尾。以此类推,直到所有的元素均排序完毕。
public static voidchoice(int[] a){
for(inti=0 ;i<a.length-1; i++){
for(intj=i+1; j<a.length; j++){
if(a[i]>a[j]){
inttemp= a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
for(intk=0;k<a.length; k++){
System.out.print(a[k]+"");
}
}
插入排序
1.从第一个元素开始,认为已经排序了;
2.取下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果前者大于后者,则交换二者位置;
4.重复步骤3,直到前面的小于等于后面的;
5.把原始序列的指针向后移一位,重复步骤2。
public static voidinsert(int[] a){
for(inti=0 ;i<a.length; i++){
for(intj=i; j>0 ; j--){
if(a[j]<a[j-1]){
inttemp= a[j];
a[j]=a[j-1];
a[j-1]=temp;
}
}
}
for(intk=0;k<a.length; k++){
System.out.print(a[k]+"");
}
}
希尔排序
1.通过设定的增量值,将数据进行分组;
2.然后对每一组数据进行排序(每组的排序其实就是插入排序);
3.增量不断变化,直至为1(要求最后一个增量必须为1);
4.每组数据排好后,整体序列自然有序了。
public static voidshell(int[] a){
for(intincrement = a.length/2; increment>0; increment/=2){
for(inti=increment; i<a.length; i++){
for(intj=i; j>=increment ; j-=increment){
if(a[j]<a[j-increment]){
inttemp= a[j];
a[j]=a[j-increment];
a[j-increment]=temp;
}
}
}
}
for(intk=0;k<a.length; k++){
System.out.print(a[k]+"");
}
}
快速排序
快速排序采用分治策略。分治法的基本思想是:将原问题分为若干个规模更小但结构相似的子问题,递归地求解这些子问题,然后将这些子问题的解组合为原问题的解。
具体思路见5月8日笔记。
快排具体思路:
以这个思路为准。
1. 设置两个变量i,j,其初值分别为low和high。分别表示待排序序列的起始下标和终止下标。
2.将第i个记录当做枢纽元。即定义pivot=r[i]。
3.从下标为j的位置开始由后向前依次搜索,当j位置处的元素值比枢纽元pivot的值小时,把该元素向前移动到下标为i的位置上,然后i=i+1;
4.从下标为i的位置开始由前向后依次搜索,当找到第1个比pivot的值大的元素时,把该元素向后移动到下标为j的位置上;然后j=j-1;
5.重复第3,4步,直到i==j为止。
6.r[i]=pivot;
7.进行一趟快速排序后,再分别对枢纽元分割所得的两个子序列进行快速排序,递归进行。
算法的框架为:
public static voidQuickSort(int[] a,int low, int high){
//对a[...]进行排序
int index= (low + high)/2;//枢纽元所在的位置
if(low<high){//仅当区间长度大于1时才需排序
Partition(a, low , high);//对a[]进行划分,元素具体怎么换位
QuickSort(a, low, index-1);//一次划分结束后,对枢纽元左边的序列进行递归排序。
QuickSort(a, index+1, high);//对枢纽元右边的序列进行递归排序。
}
}
public static voidPartition(int[] a, int low, int high) {
1.把枢纽元和最右边的元素互换位置。
2.给数组第一个元素一个index,i;给数组倒数第二个元素一个index,j。
3.把i向右移,直到碰到元素的值大于等于枢纽元为止。
4.把j向左移,直到碰到元素的值小于等于枢纽元为止。
5.当i和j交错后,也停止移动。
6.最后一步就是将枢纽元与i所指向的元素互换位置。
在这个第一阶段就实现了把所有比枢纽元小的元素都移到枢纽元的左边;把所有比枢纽元大的元素都移到枢纽元的右边。
}
快排具体实现:
public static voidqSort(int[] arr, int low, int high){
//快排入口
if(low<high){
intpivot = Partition(arr, low, high);
qSort(arr,low, pivot-1);
qSort(arr,pivot+1, high);
}
}
public static intPartition(int[] arr, int i, int j){
//一趟快排
intpivot = arr[i];
while(i<j){
while(i<j&& pivot<arr[j]){
j--;
}
if(i<j){
arr[i]= arr[j];
i++;
}
while(i<j&& arr[i]<pivot){
i++;
}
if(i<j){
arr[j]= arr[i];
j--;
}
}
arr[i]= pivot;
returni;
}
归并排序
将两个已经排序好的序列合并成一个序列。
详细内容见5月8日笔记。
注意:归并排序的空间复杂度为O(n)。因为多引入一个数组,所以需要长度为n的额外存储空间嘛。
堆排序
1.堆排序中使用到二叉堆的结构性质:
二叉堆是一棵被完全填满的二叉树,底层例外。就是有些叶子节点可能没有。底层上的元素是从左至右填充的。就是缺的话,也是右边的缺。
可以用一个数组来表示二叉堆的节点存储情况:
对数组中任一位置i上的元素,其左儿子在位置2i上,右儿子在左儿子后面的单元(2i+1)中,它的父亲则在位置|_i/2_|上。
2.涉及到的二叉堆的堆序性质:
小顶堆性质:
堆中,对于每一个节点X,X的父亲中的关键字都不大于X的关键字,根节点除外。
由这个性质,最小元总可以在根处找到。
大顶堆性质与其相反,根结点处元素的值最大。
又考虑到deleteMin这个方法。最小元可以肯定是在根处,用deleteMin方法从堆中删去最小元后,堆缩小。从而数组的最后一个单元可以存放刚刚删去的元素。依次理论一直往下走,最终,原数组将以递减的次序排列这些元素。
想递增的排列,deleteMin换成deleteMax就行了。
因为deleteMin方法花费时间O(logn),对N个元素操作一遍,总的运行时间是O(nlogn)。
堆排序两步走:
1.首先构建一个大顶堆或小顶堆出来。
建堆时是从N/2-1的位置从后往前进行筛选。父结点和两个孩子结点比较。
2.堆代表的二叉树数组的根结点和数组的最后一个结点交换位置。如此循环进行,直到二叉树剩下一个结点为止。
如果是大顶堆,堆排序后是升序排序。如果是小顶堆,堆排序后是降序排序。