排序算法

一、排序的基本概念
排序:就是将记录按关键字递增(递减)的次序排列起来,形成新的有序序列,称为排序。设n个记录的序列为{R 1,R 2,…,R n},其相应关键字序列为{K 1,K 2,…,K n},需确定一种排序P 1,P 2,…,P n,使其相应的关键字满足递增(升序),或递减(降序)的关系:
Kp 1 £ Kp 2 £ ... £ Kp n 

Kp 1 ³ Kp 2 ³ ³ Kp n
根据排序元素所在位置的不同,排序分: 内排序和外排序。
内排序:在排序过程中,所有元素调到内存中进行的排序,称为内排序。内排序是排序的基础。内排序效率用比较次数来衡量。按所用策略不同,内排序又可分为插入排序、选择排序、交换排序、归并排序及基数排序等几大类。
外排序:在数据量大的情况下,只能分块排序,但块与块间不能保证有序。外排序用读/写外存的次数来衡量其效率。
排序算法的稳定性:若待排序的序列中,存在多个具有相同关键字的记录,经过排序,这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对次序发生了改变,则称该算法是不稳定的。
排序算法的存储结构通常有三种:一维数组;链表;辅助表(如索引表)。
二、插入排序(Insertion Sort
基本思想: 将n个元素的数列分为已有序和无序两个部分。每次处理就是将无序数列的第一个元素与有序数列的元素从后往前逐个进行比较,找出插入位置,将该元素插入到有序数列的合适位置中。
2.1 直接插入排序(线性插入排序,稳定)
     基本思想:每次将一个待排序的数据元素,插入到前面已经排好序的数列中的适当位置,使数列依然有序;直到待排序数据元素全部插入完为止。
    直接插入排序是一种最简单的排序方法。具体算法步骤:
    Step1 从有序数列{a 1}和无序数列{a 2,a 3,…,a n}开始进行排序;
    Step2 处理第i个元素时(i=2,3,…,n),数列{a 1,a 2,…,a i-1}是已有序的,而数列{a i,a i+1,…,a n}是无序的。用a i与a i-1、a i-2,…,a 1进行比较,找出合适的位置将a i插入。
    Step3 重复Step2,共进行n-1的插入处理,数列全部有序。
    直接插入排序的时间复杂度为O(n 2),空间复杂度为O(1)。
【示例】:
[初始关键字] [49] 38 65 97 76 13 27 49
J=2(38) [38 49] 65 97 76 13 27 49
J=3(65) [38 49 65] 97 76 13 27 49
J=4(97) [38 49 65 97] 76 13 27 49
J=5(76) [38 49 65 76 97] 13 27 49
J=6(13) [13 38 49 65 76 97] 27 49
J=7(27) [13 27 38 49 65 76 97] 49
J=8(49) [13 27 38 49 49 65 76 97]
 
template  < class  T >
void  insertion_sort(T  * Array,  int  Size,  bool  Ascending  =   true )
{
    assert(Array 
!= NULL);
    assert(Size 
>= 2);
    
    
int i, temp;

    
if (Ascending)
    
{
        
for (i = 1; i < Size; i++)
        
{
            temp 
= Array[i];
            
while (temp < Array[i-1&& i > 0)
            
{
                Array[i] 
= Array[i-1];
                i
--;
            }

            Array[i] 
= temp;
        }

    }

    
else
    
{
        
for (i = 1; i < Size; i++)
        
{
            temp 
= Array[i];
            
while (temp > Array[i-1&& i > 0)
            
{
                Array[i] 
= Array[i-1];
                i
--;
            }

            Array[i] 
= temp;
        }

    }

}
三、选择排序(不稳定)
1. 基本思想:
  每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在已排好序的数列的最后,直到全部待排序的数据元素排完。
2. 排序过程:
3、时间复杂度:时间复杂度为O(n 2)。
【示例】:
初始关键字 [49 38 65 97 76 13 27 49]
第一趟排序后 13 [38 65 97 76 49 27 49]
第二趟排序后 13 27 [65 97 76 49 38 49]
第三趟排序后 13 27 38 [97 76 49 65 49]
第四趟排序后 13 27 38 49 [49 97 65 76]
第五趟排序后 13 27 38 49 49 [97 97 76]
第六趟排序后 13 27 38 49 49 76 [76 97]
第七趟排序后 13 27 38 49 49 76 76 [ 97]
最后排序结果 13 27 38 49 49 76 76 97

template <class T>
void swap(T *A, T *B)
{
    T temp;

    temp 
= *A;
    
*= *B;
    
*= temp;
}


template 
<class T>
void selection_sort(T *Array, int Size, bool Ascending = true)
{
    assert(Array 
!= NULL);
    assert(Size 
>= 2);

    
int i, j;

    
if (Ascending)
    
{
        
for (i = 0; i < Size-1; i++)
            
for (j = i+1; j < Size; j++)
                
if (Array[j] < Array[i])
                    swap(
&Array[i], &Array[j]);
    }

    
else
    
{
        
for (i = 0; i < Size-1; i++)
            
for (j = i+1; j < Size; j++)
                
if (Array[j] > Array[i])
                    swap(
&Array[i], &Array[j]);
    }

        
}

四、冒泡排序(BubbleSort)(稳定)
1. 基本思想:
  两两比较待排序数据元素的大小,发现两个数据元素的次序相反时即进行交换,直到没有反序的数据元素为止。
2. 排序过程:
   step1 将关键字按纵向排列,然后自下而上地对每两个相邻的关键字进行比较,若饿日为逆序(即k j-1>k j),则将两个记录交换位置,这样的操作反复进行,直至全部记录都比较、交换完为止。一趟冒泡处理,就将关键字最小的记录安排在第一记录的位置上。
   step2 对后n-1个记录重复同样操作,再将次小关键字记录安排在第二个记录的位置上。
   Step3 重复上述过程直至没有记录需要交换为止。
3、算法分析:最坏情况,比较(n 2-n)/2次,移动(n 2-n)*3/2。算法时间复杂度O(n 2)。
【示例】:
49 13 13 13 13 13 13 13
38 49 27 27 27 27 27 27
65 38 49 38 38 38 38 38
97 65 38 49 49 49 49 49
76 97 65 49 49 49 49 49
13 76 97 65 65 65 65 65
27 27 76 97 76 76 76 76
49 49 49 76 97 97 97 97
在冒泡排序的程序中,按升序排列使用了改进的冒泡排序算法,是得最好情况下时间复杂度为O(n),降序排列部分使用的是最基础冒泡排序

template  < class  T >
void  bubble_sort(T  * Array,  int  Size,  bool  Ascending  =   true )
{
    assert(Array 
!= NULL);
    assert(Size 
>= 2);

    
int i, j, k = Size;

    
if (Ascending)
    
{
        
for (i = 1; i < Size; i = k)
            
for (j = 0; j < Size-i; j++)
                
if (Array[j] > Array[j+1])
                
{
                    swap(
&Array[j], &Array[j+1]);
                    k 
= Size - j;
                }

    }

    
else
    
{
        
for (i = 1; i < Size; i++)
            
for (j = 0; j < Size-i; j++)
                
if (Array[j] < Array[j+1])
                    swap(
&Array[j], &Array[j+1]);
    }

}

五、快速排序(Quick Sort)(不稳定)
1. 基本思想:
   在当前无序区R[1..H]中任取一个数据元素作为比较的"基准"(不妨记为X),用此基准将当前无序区划分为左右两个较小的无序区:R[1..I- 1]和R[I+1..H],且左边的无序子区中数据元素均小于等于基准元素,右边的无序子区中数据元素均大于等于基准元素,而基准X则位于最终排序的位置 上,即R[1..I-1]≤X.Key≤R[I+1..H](1≤I≤H),当R[1..I-1]和R[I+1..H]均非空时,分别对它们进行上述的划 分过程,直至所有无序子区中的数据元素均已排序为止。
2. 排序过程:
3、算法分析:最坏情况的时间复杂度为O(n 2),最好情况时间复杂度为O(nlog 2n)。
【示例】:
初始关键字 [49 38 65 97 76 13 27 49]
第一次交换后 [27 38 65 97 76 13 49 49]
第二次交换后 [27 38 49 97 76 13 65 49]
J向左扫描,位置不变,第三次交换后 [27 38 13 97 76 49 65 49]
I向右扫描,位置不变,第四次交换后 [27 38 13 49 76 97 65 49]
J向左扫描 [27 38 13 49 76 97 65 49]
(一次划分过程)
初始关键字 [49 38 65 97 76 13 27 49]
一趟排序之后 [27 38 13] 49 [76 97 65 49]
二趟排序之后 [13] 27 [38] 49 [49 65]76 [97]
三趟排序之后 13 27 38 49 49 [65]76 97
最后的排序结果 13 27 38 49 49 65 76 97
快速排序也是分治策略的一种典型体现:将待排序数据划分成更小的序列,对每个划分的序列进行交换。这里所谓的交换是指在单独的一个序列里面,选取一个枢纽 (pivot),以此枢纽为划分点,将该序列里面所有小于枢纽的数据放于枢纽的左边,大于枢纽值的数据放于枢纽值的右边。
    枢纽的选择有多种,一般有四种选择:选取序列第一个元素、选取序列最后一个元素、选取序列(First +Last)/2个元素、随机选取一个元素。在本文代码中,我们选取的是第一个元素。
template  < class  T >
int  partition(T  * Array,  int  First,  int  Last)
{
    T temp 
= Array[First];

    
while (First < Last)
    
{
        
while (First < Last && Array[Last] >= temp)
            Last
--;
        Array[First] 
= Array[Last];

        
while (First < Last && Array[First] <= temp)
            First
++;
        Array[Last] 
= Array[First];
    }


    Array[First] 
= temp;
    
return First;
}


template 
< class  T >
void  quick_sort(T  * Array,  int  First,  int  Last)
{
    
int p;

    
if (First < Last)
    
{
        p 
= partition(Array, First, Last);
        quick_sort(Array, First, p 
- 1);
        quick_sort(Array, p 
+ 1, Last);
    }

}

五、堆排序(Heap Sort)
1. 基本思想:
堆排序是一树形选择排序,在排序过程中,将R[1..N]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。
2. 堆的定义: N个元素的序列K1,K2,K3,...,Kn.称为堆,当且仅当该序列满足特性:
Ki≤K2i Ki ≤K2i+1(1≤ I≤ [N/2])
堆 实质上是满足如下性质的完全二叉树:树中任一非叶子结点的关键字均大于等于其孩子结点的关键字。例如序列10,15,56,25,30,70就是一个堆, 它对应的完全二叉树如上图所示。这种堆中根结点(称为堆顶)的关键字最小,我们把它称为小根堆。反之,若完全二叉树中任一非叶子结点的关键字均大于等于其 孩子的关键字,则称之为大根堆。
3. 排序过程:
堆排序正是利用小根堆(或大根堆)来选取当前无序区中关键字小(或最大)的记录实现排序 的。我们不妨利用大根堆来排序。每一趟排序的基本操作是:将当前无序区调整为一个大根堆,选取关键字最大的堆顶记录,将它和无序区中的最后一个记录交换。 这样,正好和直接选择排序相反,有序区是在原记录区的尾部形成并逐步向前扩大到整个记录区。
【示例】:对关键字序列42,13,91,23,24,16,05,88建堆




  堆排序在最差情况下相比快速排序,具有更好的时间复杂度。
template  < class  T >
void  adjust_heap(vector < T >   & Vct,  int  First,  int  Last)
{
    
int CurrentPos = First;
    
int ChildPos = 2 * CurrentPos + 1;
    T Temp 
= Vct[First];

    
while (ChildPos <= Last-1)
    
{
        
if (ChildPos+1 <= Last-1 && Vct[ChildPos] < Vct[ChildPos+1])
            ChildPos 
+= 1;

        
if (Temp < Vct[ChildPos])
        
{
            Vct[CurrentPos] 
= Vct[ChildPos];
            CurrentPos 
= ChildPos;
            ChildPos 
= 2 * CurrentPos + 1;
        }

        
else
            
break;
    }


    Vct[CurrentPos] 
= Temp;
}


template 
< class  T >
void  make_heap(vector < T >   & Vct)
{
    
int LastPos = Vct.size();
    
int HeapPos = (LastPos - 2/ 2;

    
while (HeapPos >= 0)
    
{
        adjust_heap(Vct, HeapPos, LastPos);
        HeapPos
--;
    }

}


template 
< class  T >
void  heap_sort(vector < T >   & Vct)
{
    make_heap(Vct);
    
int n = Vct.size();

    
for (int i = n; i > 1; i--)
        pop_heap(Vct, i);
}

六、几种排序算法的比较和选择
1. 选取排序方法需要考虑的因素:
(1) 待排序的元素数目n;
(2) 元素本身信息量的大小;
(3) 关键字的结构及其分布情况;
(4) 语言工具的条件,辅助空间的大小等。
2. 小结:
(1) 若n较小(n <= 50),则可以采用直接插入排序或直接选择排序。由于直接插入排序所需的记录移动操作较直接选择排序多,因而当记录本身信息量较大时,用直接选择排序较好。
(2) 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
(3) 若n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序。 快速排序是目前基于比较的内部排序法中被认为是最好的方法。
(4) 在基于比较排序方法中,每次比较两个关键字的大小之后,仅仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以证明:当文件的n个关键字随机分布时,任何借助于"比较"的排序算法,至少需要O(nlog2n)的时间。
(5) 当记录本身信息量较大时,为避免耗费大量时间移动记录,可以用链表作为存储结构。
七、 归并排序(稳定)
    归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表:即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序子序列合并为整体有序序列。
    将已有序的子序列合并,得到完全有序的序列:即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
归并排序具体步骤:
Step1 把待排序的n个记录看作是长度为1的有序序列。将相邻子序列两两归并为长度为2的有序序列;
Step2 把得到的n/2个长度为2的有序子序列再归并为长度为 2*2 的有序序列;
Step3 按Step2的方式,重复对相邻有序子序列进行归并操作,直到成为一个有序序列为止。
分为两步,一是划分待排序数据,二是归并划分的子序列,归并排序是一个递归过程。

template  < class  T >
void  merge(T  * Array,  int  First,  int  Mid,  int  Last)
{
    T 
*temp = new T[Last - First + 1];
    assert(temp 
!= NULL);
    
    
int indexA = First;
    
int indexB = Mid + 1;
    
int indexT = 0;

    
while (indexA <= Mid && indexB <= Last)
        temp[indexT
++= (Array[indexA] < Array[indexB]) ? Array[indexA++] : Array[indexB++];

    
while (indexA <= Mid)
        temp[indexT
++= Array[indexA++];

    
while (indexB <= Last)
        temp[indexT
++= Array[indexB++];

    indexA 
= First;
    
for (indexT = 0; indexT <= Last-First; indexT++, indexA++)
        Array[indexA] 
= temp[indexT];

    delete [] temp;
}


template 
< class  T >
void  merge_sort(T  * Array,  int  First,  int  Last)
{
    
int mid;

    
if (First < Last)
    
{
        mid 
= (First + Last) / 2;
        merge_sort(Array, First, mid);
        merge_sort(Array, mid
+1, Last);
        merge(Array, First, mid, Last);
    }

}
八、 希尔排序(缩小增量排序,不稳定)
    希尔排序是一种快速排序法,出自D.L.Shell。基本思想是:
    仍采用插入法,排序过程通过调换并移动数据项来实现。先取一个小于n的整数d 1并作为第一个增量,将文件的全部记录分成d 1个组,所有距离为d1倍数的记录放在同一个组中,在各组内进行直接插入排序;然后取第二个增量d 2<d 1,重复上述的分组和排序,直至所取的增量d t=1(d t<d t-1<...<d 2<d 1)为止,此时,所有的记录放在同一组中进行直接插入排序。增量一般d按给定公式减少: d i+1 =(d i + 1)/2 ,直到d等于1为止。
例,有关键字序列{5,4,3,6,7,1,8,9}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值