您的位置:首页 > 编程语言

常见排序算法原理及代码实现

2013-01-05 11:04 246 查看
排序算法
(一)
【冒泡排序】(Bubble Sort)

冒泡排序方法是最简单的排序方法。这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。在冒泡排序算法中我们要对这个“气泡”序列处理若干遍。所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确。如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置。显然,处理一遍之后,“最轻”的元素就浮到了最高位置;处理一遍之后,“次轻”的元素就浮到了次高位置。在作第二遍处理时,由于最高位置上的元素已是“最轻”元素,所以不必检查。一般地,第i遍处理时,不必检查第i高位置以上的元素,因为经过前面i-1遍的处理,它们已正确地排好序。

Java实现
/**
*
冒泡排序
*
相邻两个进行比较,每次排完后,该次的最后一个总是最大的
*/
publicint [] bubbleSort(int
[]array){
for(int i = 0 ; i < array.length
; i++){ for(int j = 1 ;j < array.length-i
; j++){ if(array[j-1]> array[j]){
array = SortUtil.swap(array,j-1, j);//相邻两个进行比较,找到该次最大的
}
}
}
return array;

}

算法复杂度
程序执行的次数:(n-1)+(n-2)+…1=n(n-1)/2
所以算法时间复杂度是O(n2)。

(二)
【插入排序】(Insertion Sort)
插入排序的基本思想是,经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L插入L[1..i-1]的适当位置,使得L[1..i]又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L和L[i-1],如果L[i-1]≤ L,则L[1..i]已排好序,第i遍处理就结束了;否则交换L与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。

插入排序的基本操作
Eg:
A) {48},62, 35, 77, 55, 14, 35, 98
B) {48 ,62} 35, 77, 55, 14, 35, 98
C) {35 ,48 ,62} 77, 55, 14, 35, 98
D) {35 ,48 ,62 , 77} 55, 14, 35, 98
E) {35 ,48 ,55, 62, 77} 14, 35, 98
F) {14 ,35 ,48 ,55 , 62, 77} 35, 98
G) {14 ,35 ,35, 48 ,55 , 62, 77} 98
H) {14 ,35 ,35, 48 ,55 , 62, 77 ,98}

Java实现

/*
*插入排序
*第i遍处理时,前面i-1个已经排好顺序,只需把第i个插入到适当的位置
*
*/
publicint [] insertSort(int
[]array){
for(int i = 0 ; i < array.length-1
;i++){
for(int j = i+1 ;j < array.length
;j++){
while(array[j-1]>array[j]){
array = SortUtil.swap(array,j-1, j);
//j从i,i-1,i-2...依次比较,找到相应的位置
if(j != 1){
j--;
}
}
}
}
return array ;

算法复杂度

最好的情况为:待排序的记录为有序序列,每次只需比较一次,且无需移动元素
最坏的情况为:第i次需要移动的次数为:i-1,(1<i<n-1),所以总共移动的次数为:(1+2+…(n-2))=(n-1)(n-2)/2

算法时间复杂度是O(n2)
} }
(三)

【选择排序】(Selection Sort)

选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第 i 遍处理是将[i..n]中最小者与位置 i 交换位置。这样,经过 i 遍处理之后,前 i 个记录的位置已经是正确的了

选择排序的基本操作
Eg:
A) {48,62, 35, 77, 55, 14, 35, 98}
B) 14 {48,62, 35, 77, 55, 35, 98}
C) 14, 35{48, 62,77, 55, 35, 98 }
D) 14, 35,35, {48, 62,77, 55, 98 }
E) 14, 35,35,48 { 62,77, 55, 98 }
F) 14, 35,35,48,55 { 62,77, 98 }
G) 14, 35,35,48,55,62 {77, 98 }
H) 14, 35,35,48,55,62,77, 98

Java实现
/*
*
选择排序第i次排序完,第i个位置的元素总是本次里面最小的
*/

publicint[] selectSort(int
array[]) {
int key = 0;
for (int i = 0; i < array.length
- 1; i++) {
key = i;
// 第i次循环,先假设array[i]为本次最小的元素
for (int j = i + 1; j < array.length;
j++) {
if (array[j]< array[key]) {
// i后面的元素与array[i]比较找出第i次最小的
key = j;
}

}
if (key !=i) {
array =
SortUtil.swap(array, i,key); //如果第i个位置的元素不是最小的,则将它与最小的交换
}

}
return array;
}

算法复杂度

比较次数: (n-1)+(n-2)+……1=n(n-1)/2
所以算法时间复杂度是O(n2 )。

(四)
【快速排序】(Quick Sort)

快速排序是对冒泡排序的一种改进。它的基本思想是:通过一躺排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一不部分的所有数据都要小,然后再按次方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快排基本操作

假设要排序的数组是A[1]……A
,首先任意选取一个数据(通常选用第一个数据)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一躺快速排序。一躺快速排序的算法是:
1)、设置两个变量I、J,排序开始的时候I:=1,J:=N;
2)以第一个数组元素作为关键数据,赋值给X,即X:=A[1];
3)、从J开始向前搜索,即由后开始向前搜索(J:=J-1),找到第一个小于X的值,两者交换;
4)、从I开始向后搜索,即由前开始向后搜索(I:=I+1),找到第一个大于X的值,两者交换;
5)、重复第3、4步,直到I=J;
例如:待排序的数组A的值分别是:(初始关键数据X:=49)
A[1] A[2] A[3] A[4] A[5] A[6] A[7]:
49 38 65 97 76 13 27
进行第一次交换后: 27 38 65 97 76 13 49
( 按照算法的第三步从后面开始找
进行第二次交换后: 27 38 49 97 76 13 65
( 按照算法的第四步从前面开始找>X的值,65>49,两者交换,此时I:=3)
进行第三次交换后: 27 38 13 97 76 49 65
( 按照算法的第五步将又一次执行算法的第三步从后开始找
进行第四次交换后: 27 38 13 49 76 97 65
( 按照算法的第四步从前面开始找大于X的值,97>49,两者交换,此时J:=4 )
此时再执行第三不的时候就发现I=J,从而结束一躺快速排序,那么经过一躺快速排序之后的结果是:
27 38 13 49 76 97 65,
即所以大于49的数全部在49的后面,所以小于49的数全部在49的前面。
快速排序就是递归调用此过程——在以49为中点分割这个数据序列,分别对前面一部分和后面一部分进行类似的快速排序,从而完成全部数据序列的快速排序,最后把此数据序列变成一个有序的序列。
进行一次快速排序之后划分为 {27 38 13} 49 {76 97 65}
分别对前后两部分进行快速排序 {13} 27 {38}
结束 结束 49 {65 76 97}
49 {65} 76 {97}
结束 结束

Java实现
/*
*
快速排序一次排序后,前面的元素总是比该次的关键值key小,而后面的元素总比该key大,在递归调用前面的排序
*/

publicint sort(int[]
array, int left,
int right) {
int key =array[left];
// 取关键值,为分段的首个元素
int low = left;
int high =right;
while (low <high) {
while (low <high && array[high] >= key) {
high--;
}

if (low <high) {
// 从右到左找到小于key的记录
array[low] = array[high];
low++;
}

while (low <high && array[low] <= key) {
low++;
}
if (low <high) {
// 从左到右找到大于key的记录
array[high] = array[low];
high--;
}

}

array[low] = key;
System.out.println("low:" + low +
"high:" + high);
return low;

}

publicvoid quickSort(int[]
array, int low,
int high) {
if (low <high) {
int pos =
this.sort(array, low, high);
// 一趟比较结束后,以key为值将数组分成前后两部分
quickSort(array, low, pos - 1);
// 对前面一部分进行快排
quickSort(array, pos + 1, high);//
对后面一部分进行快排
}
}

时间复杂度

最理想情况下,每趟将序列一分两半,正好在表中间,将表分成大小相同的子表。
时间复杂度计算如下:
n个数1次划分成2个
2次划分成4个
3次划分成8个
那么多少次才能将这n个数划分成1个序列呢?
也就是2的多少次幂等于或者接近n
所以就有划分次数为log 2 (n)
因为每个划分段都要比较n次,所以一共比较次数是n*划分段数log2(n)
时间复杂度就为:nlog2(n)

最坏情况下待排序的记录为已排好序的,第一趟比较的次数为(n-1),比较完后,左面的序列为一个空序列,右边的序列长度为:n-1,所以第二趟比较的次数为为(n-2)…
所以比较的总次数为: (n-1)+(n-2)+…1=n(n-1)/2,时间复杂度为:O(n^2)

(五)
【堆排序】(Heap Sort)
堆排序是一种树形选择排序,在排序过程中,将A
看成是完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。

什么是堆

堆是一种数据结构,是一棵完全二叉树且满足性质:所有非叶子结点的值均不大于或均不小于其左、右孩子结点的值,如下是一个堆得示例:

堆排序基本操作
Eg:A[5]={2,8,5,3,1}
(1)建成一个完全二叉树如下:

(2)将完全二叉树调整为一个大跟堆(从n/2,开始调整,因为n/2是最后一个有子孩子的节点)
A[6]={3,5,8,9,1,2},
以8为根结点调整堆后:因为8>2,此处不进行记录移动操作
以5为根结点调整堆后:5<9,5跟9互换
A[6]={3,9,8,5,1,2}
以3为根结点调整堆后:3<9,3跟9互换
A[6]={9,3,8,5,1,2}
以9为根的左子树不满足大跟堆的性质,所以以3为跟调整堆,即交换3和5,得A[6]={9,5,8,3,1,2}

(3)排序交换的过程
2.9和2交换,然后把9从堆中去掉后:
A[6]={2,3,8,5,1,9}
3.筛选法调整堆A[5]={2,3,8,5,1}后(调整过程参见上面):
A[6]={8,3,2,5,1,9}
4.堆顶记录与最后一个记录互换,重复第二步。

Java实现
/*
*
*
堆排序(1)建立一个堆,并将其调整为一个大根堆
* (2)首位和最后一位交换,然后调整堆
*/
//
新建一个堆
publicint[] buildHeap(int
array[], int length) {
intlastHasChildIndex = (int) (length /
2); // 找到最后一个有孩子的节点下标
for (int i = lastHasChildIndex; i >=
0; i--) {
array = this.myShift(i,array, length);
//将新建的堆调整为一个大根堆
}
return array;
}

//
调整为一个大根堆
publicint[] myShift(inthasChildIndex,
int[] array,
int length) {
int parentValue= array[hasChildIndex];
int leftChildIndex = 2 * hasChildIndex + 1;
while(leftChildIndex <= length) {
//
如果右孩子大于左孩子,则只需要将右孩子与父节点交换即可
if(leftChildIndex + 1 <= length) {
if(array[leftChildIndex + 1] > array[leftChildIndex]) {
leftChildIndex++;
}
}
//如果父节点大于子节点,则该节点不需要调整,相反则将父节点与子节带你交换
if(parentValue >= array[leftChildIndex]) {
break;
} else {
array = SortUtil.swap(array,hasChildIndex, leftChildIndex);
//交换后还要判断以子节点为根节点的节点是否需要调整
hasChildIndex = leftChildIndex;
leftChildIndex = 2 * hasChildIndex+ 1;
}

}
// [48,62, 35, 98, 55, 14, 35, 77]
return array;
}

publicint[] heapSort(int
array[]) {
int length =array.length - 1;
array = buildHeap(array, length);//建初堆,并调整成为一个大跟堆
while (length> 0) {
//首位和最后一位交换,,然后在调整为一个大根堆,接着再交换调整
SortUtil.swap(array, 0,length);
length--;
array = this.buildHeap(array,length);

}
return array;
}

调整堆时的次数不会超过完全二叉树的深度:([log2n]向下取整+1),其中n为结点个数,2为底数,所以一次调整执行的次数为log2(n),而比较的次数为(n/2-1),所以总的比较次数为(n/2-1)log2(n)
算法时间复杂度就为:O(nlog2n)。

(六)

【希尔排序】希尔排序(Shell Sort)是插入排序的一种。是针对直接插入排序算法的改进。该方法又称缩小增量排序

希尔排序的基本操作:

  先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成(n除以d1)个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。

  该方法实质上是一种分组插入方法。
  给定实例的shell排序的排序过程
  假设待排序文件有8个记录,其关键字分别是:
  48, 62, 35, 77, 55,14,35,98
  增量序列的取值依次为:
4,2,1
Java实现
/*
*
希尔排序以不同的间隔,对元素进行插入排列
*/
publicint[] shellSort(int[]
array, int del) {
while (del >=1) {
System.out.println("del:" + del);
for (int i = 0; i < array.length
- 1; i++) {
for (int j = i + del; j < array.length;
j=j+del) {
while((j-del>=0)&&array[j - del] > array[j]) {
//
对第i个位置和(i+del)...的元素进行插入排序
array = SortUtil.swap(array,j, j - del);

j = j - del;

}
}
}
del = (int) (del / 2);//
缩小间隔,重新进行排序

}
return array;
}

算法的时间复杂度:当d=1时,希尔排序相当于插入排序,但因为逆序数很少,所以移动的次数相对于插入排序也会减少。因此希尔排序是一个比较好的直接插入排序。它的时间复杂度为(n^1.5)

(七)

【归并排序】

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并操作

 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。
  如 设有数列{6,202,100,301,38,8,1}
  初始状态: [6] [202] [100] [301] [38] [8] [1]

i=1 [6 202 ] [ 100 301] [ 8 38] [ 1 ]
i=2 [ 6 100 202 301 ] [ 1 8 38 ]
i=3[1 6 8 38 100 202 301 ]

Java实现

/*
*
归并排序归并的长度分别为2,4...(array.length)
*/

publicint[] mergeSort(int[]
array) {
int index = 2;
while (index<= array.length) {
for (int i = 0; i < array.length
- 1; i = i+ index) {
while (i + index> array.length) {
index--;
}
int begin = i;
// 归并分段的每一段的开始位置
int end = i +index;
// 归并分段的每一段的结束
System.out.println("begin:" + begin +
"end:" + end);
while (begin <end) {
if (begin + 1< end) {
for (int m = begin; m <
end; m++) {
for (int n = begin +
1; n < end; n++) { // 对分段的每一部分内部排序
if (array
< array[n - 1]) {

SortUtil.swap(array, n -1, n);
}
}
}

}
begin++;

}

}
index = index * 2;
// 归并的长度增长为原来的2倍

}

return array;
}

时间复杂度:一趟归并操作,是调用(n/d)次merge,总共的合并次数为m(m=log2(n)),所以总的执行次数为:(n/d)*log2(n)
所以时间复杂度为:n*log2(n)

各种排序的比较

排序方法
平均时间
稳定性
插入排序
O(n2)
稳定
冒泡排序
O(n2)
稳定
简单选择排序
O(n2)
不稳定
希尔排序
O(n2)
不稳定
快速排序
O(nlogn)
不稳定
堆排序
O(nlogn)
不稳定
归并排序
O(nlogn)
稳定
备注:
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。

不同条件下,排序方法的选择

(1)若n较小(如n≤50),可采用直接插入或直接选择排序。

 当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。

(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;

(3)若n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或

归并排序。

快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;

若要求排序稳定,则可选用归并排序。但从单个记录起进行两两归并的
排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: