排序算法是数据结构的一个很重要的部分,目前最常用的排序算法有:冒泡排序、简单选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序、基数排序。下面将分别介绍这几种排序算法,并给出相应的代码实现。
1、冒泡排序
冒泡排序,即每次对比相邻两个元素,将大的元素向后移动,以从前向后冒泡为例子,第一循环结束,最大的元素会被移动到最后的位置,第二轮循环结束,次大(第二大)的元素会被移动到倒数第二个位置,一次循环。冒泡排序的关键就是每次循环结束的边界判断条件。
public void swap(int[] List, int i, int j) { //交换i,j对应位置的值
int tem = List[i];
List[i] = List[j];
List[j] = tem;
}
从前向后冒泡
public void BubbleFromFirstToEnd(int[] List){
int i,j;
for(i= List.length-1; i > 0; i--){
for(j = 0; j < i; j++){
if(List[j] > List[j+1]){
swap(List, j, j+1);
}
}
}
}
从后向前冒泡
public void BubbleFromEndToFirst(int[] List){
int i,j;
for(i = 0; i < List.length-1; i++){
for(j = List.length-1; j > i; j--){
if(List[j-1] > List[j]){
swap(List,j-1,j);
}
}
}
}
冒泡排序的改进
1、添加标志域Flag,如果一次扫描到边界时没有发生交换,说明序列已经有序,不需要再进行后续的扫描
public void BubbleOptimized(int[] List){
int i,j;
boolean flag = true;
for(i = 0; i < List.length-1 && flag; i++){
flag = false;
for(j = List.length-1; j > i; j--){
if(List[j-1] > List[j]){
swap(List,j-1,j);
flag = true;
}
}
}
}
2、双向冒泡
public void DualBubble(int[] List){
int left,right;
int count = 0;
left = 0;
right = List.length-1;
while(left < right ){
int tem = left;
for(; tem < right; tem++){
if(List[tem] > List[tem+1]){
swap(List, tem, tem+1);
count+=1;
}
}
right--;
for(; tem > left; tem--){
if(List[tem-1] > List[tem]){
swap(List, tem-1, tem);
count+=1;
}
}
left++;
}
System.out.println(count);
}
2、简单选择排序
简单选择排序的思想是每一趟循环都从未排序的序列中选取最小的值,加入有序的序列末尾。
public void SelectSort(int[] List) {
int i, j, min;
for(i = 0; i < List.length-1; i++) {
min = i; //min初始为无序队列的第一个下标
for(j = i+1; j < List.length; j++) { //对无序队列进行循环,注意是从i+1位开始
if(List[j] < List[min]) { //当前j对应元素小于min对应的值,则更新min
min = j;
}
}
if(min != i) //min == i 说明最小元素已经在第i个位置,不用交换
swap(List, i, min);
}
}
复杂度分析:
通过循环条件可以看出,简单选择排序的第 i 次循环会执行 n-i 次关键词比较,没有其他结束循环的条件,因此需要的比较次数为n(n-1)/2, 交换是每次循环结束后进行的,最好的时候交换次数为0(初始序列为升序),最差的时候交换次数为 n(初始序列为降序),因此总的时间复杂度为O[n^2].
执行过程中不需要外部存储空间,空间复杂度为O[1].
和冒泡排序相比,两者的时间复杂度相同,但是简单选择排序的交换次数要小很多,对于交换数据代价较大的问题来说,简单选择排序的性能要好于冒泡排序。
因为每次是按顺序遍历查找最小值的,因此是稳定的排序(有相同值的情况下,查找返回的是第一个最小的值)。
3、直接插入排序
直接插入排序是每次将一个 元素插入到已经排好序的有序表中。若有序序列为【1,...,i】,即将第i+1个元素加入到有序序列中。
public void Insert(int[] List) {
int i, j, tem;
for(i = 1; i < List.length; i++) {
tem = List[i];
for(j = i-1; j >= 0 && List[j] > tem; j--) { //循环查找i元素在有序序列中的位置
List[j+1] = List[j]; //大元素后移
}
List[j+1] = tem; //插入到正确位置
}
}
这里在元素交换时没有使用swap方法,因为第i次循环时所有发生的交换都会有元素i,因此需要交换时只需改变另一个元素的位置,最后再将 i 元素插入,可以减少一半的交换次数。
这里还可以采用“哨兵”的思想,即在序列前增加一个标志域flag=0,需要排序的序列下标从1开始,每次对 i 进行循环时将标志域List[0]的值设置为List[i],这样在 for( j = i-1,..., ) 时循环条件可以设置为 for( j = i- 1; List[j] > List[0]; j-- ), 这样可以不用每次都判断 j>=0,减少一半的比较时间。
复杂度分析:
空间复杂度: 可见只需要一个外部辅助空间,tem.
时间复杂度:
- 最好的情况,即序列本身为顺序(升序),那么一共需要比较 n-1 次,交换0次,时间复杂度为O[n]
- 最坏的情况,即序列本身为逆序(降序),那么一共循环 n-1次,第 i 次循环比较 i 次(包括标志域),记n录移动次数为 i +1次(包括标志域)。
因此平均时间复杂度为O[n^2], 如果序列随机排列,根据等概率原则,平均比较和移动次数为 n^2/4。因此综合性能优于冒泡和选择排序。
4、希尔排序
过程:
代码
public void Shell(int[] List) {
int i, j, tem;
int len = List.length;//序列长度
int increase = len; //增量
while(increase>1) {
increase = increase/3+1; //增量更新
for(i = increase; i < len; i++) { //increase确定时,每个increase间隔的元素序列相当于在进行直接插入排序
tem = List[i];
for(j = i - increase; j >= 0 && List[j] > tem; j -= increase) {
List[j+increase] = List[j];
}
List[j+increase] = tem;
}
}
}
算法分析:
希尔排序是对直接插入排序的改进,在插入时,通过increase(增量)实现了跳跃式的移动,使元素可以更快的移动到正确的位置,因此最优效率比直接插入排序高很多。因为是跳跃式移动,因此希尔排序时不稳定的。
希尔排序的时间复杂度在O[nlogn] 到O[n^2]之间。
空间复杂度为O[1]。
同时要注意,希尔排序增量在设置时,必须保证最后一次循环时增量为1。直接插入排序类似增量为1时的希尔排序。
5、堆排序
堆排序是对简单选择排序算法的改进,排序会通过下标对应关系构建一颗完全二叉树。大根堆每个节点的值都大于等于左右孩子节点(小根堆类似)。
算法思路:总体分两部分
- 由无序序列构建大根堆
构建过程如下面的图所示,构建大根堆时,会从最后一颗非空子树开始,不断向前进行构建,下图中,会先对节点5为根的子树进行调整,然后是节点4为根的子树,然后是节点3为根的子树 ,一直到节点1为根的树,则整个大根堆构建完成。
1)
- 不断调整大根堆,形成有序序列
每次将根元素和大根堆最后一个元素(i)的值进行交换,然后对前 i-1 个元素进行大根堆调整。
后面的图就不画了,就这样一直循环,直到大根堆的元素为空。
动态演示
代码
public void Adjust(int[] List, int start, int end) {
int tem;
int t = start;
int s = start*2;
tem = List[t];
while(s <= end) {
if(s+1 <= end && List[s+1] > List[s]) s+=1; //找左右子节点中的最大值
if(List[s] > tem) {
List[t] = List[s];
t = s;
s = s*2;
}else {
break;
}
}
List[t] = tem;
}
public void Heap(int[] List) {
int i, j;
int len = List.length-1;//序列长度,序列下标从1开始,因此长度少1.
//生成大根堆
for(i = len/2; i >= 1; i--) { //从最后一颗非空子树开始循环
Adjust(List, i, len);
}
//对大根堆进行调整排序
for(i = len; i > 1; i--) {
swap(List, 1, i);
Adjust(List, 1, i-1);
}
}
注意:这个代码在写的时候,有序数列的第一个下标按照从1开始计算,因为下标从1到n的话,刚好和完全二叉树节点的位置编号,比较容易理解,如果想从下标0开始的话,需要自行调整。
算法分析:
第 i 次重建堆时元素个数为 n-i ,重建堆的树深度为 log(n-i),共重建 n-1 次, 因此堆排序的时间复杂度为O[nlogn],
堆排序 比较和交换时在序列中时跳跃进行的,因此是不稳定的算法。
构建堆初始的比较次数较多,因此不适用于小规模的排序。
6、归并排序(MergeSort)
基本思想:通过分治策略,将问题划分为子问题,即每个序列, 从中间分为左右两个序列,将左右序列分别变为有序后,再按照有序序列合并的方法将整个序列归并为有序序列。对左右子序列同样继续进行划分,直到划分的子序列长度为1为止。
public void mergeSort(int[] List, int start, int end) {
if(end - start <1){
return;
}
int mid = (end + start)/2; //从mid进行划分
mergeSort(List, start, mid); //对左序列进行排序
mergeSort(List, mid + 1, end); //对右序列进行排序//此时左右序列都为有序,只需将左右序列进行合并即可,merge()的功能相当于合并两个有序列表。
merge(List,start, mid, mid+1, end);
}
public void merge(int[] List, int starta, int enda, int startb, int endb){
int[] tem = new int[endb - starta + 1]; //先把合并结果存储在tem中。
int ta, tb, tt;
ta = starta;
tb = startb;
tt = 0;
while(ta <= enda && tb <= endb){
if(List[ta] < List[tb]){
tem[tt++] = List[ta];
ta++;
}else{
tem[tt++] = List[tb];
tb++;
}
}
while (ta <= enda){
tem[tt++] = List[ta++];
}
while(tb <= endb){
tem[tt++] = List[tb++];
}
for(tt=0, ta = starta; tt < tem.length; tt++, ta++){
List[ta] = tem[tt];
}
}
归并排序需要递归log2n次, 因此时间复杂度为O(nlog2n)
7、快速排序(QuickSort)
基本思想:通过一趟排序将待排序记录分割成独立的两部分,前一部分的值都比后一部分小(大),然后分别对前后两部分再进行分割,直到整个序列有序。这可以利用递归的思想进行解决。因为子问题和原问题的性质都相同。
算法步骤:
1、从序列中选区一个值,作为划分“依据”(最简单的是选无序序列的第一个元素)
2、通过比较和交换,使得“依据”左侧的值比它小,右侧的值都比它大,这样“依据”就将整个序列划分成了左右两个独立的无序序列。
3、对左右两个无序序列在进行1,2的操作,直到无序序列长度为1,结束循环。
动态演示:
代码:
public int Split(int[] List, int low, int high) {
int tem = List[low];
while(low < high) {
while(low < high && List[high] >= tem) high--;
List[low] = List[high];
while(low < high && List[low] <= tem) low++;
List[high] = List[low];
}
List[low] = tem;
return low;
}
public void Quick(int[] List, int low, int high) {
int separator;
if(low < high) {
separator = Split(List, low, high); //进行分割,并获取分隔元素的下标
Quick(List, low, separator-1); //分别递归对左右序列进行递归分割。
Quick(List, separator+1, high);
}
}
算法效果评价:
快速排序是通过递归实现的,最优情况下,separator每次都可以将序列等分 ,此时递归树的深度为【log2n】+1,也就是需要递归log2n次,因此总的时间复杂度为O[nlogn],最差情况下,每次划分都只得到一个子序列(separator分割位置位于序列首或者尾部),此时递归 n-1 次,第 i 次递归需要对比 n-i 次,此时时间复杂度为O[n^2]。平均复杂度为O【nlogn】
递归底层是通过堆栈实现的,最好情况下树深度为log2n,因此空间复杂度为O[log2n] ,最差情况下,递归树深度为n-1,空间复杂度为O[n^2],平均空间复杂度为O[logn]
因为位置交换是跳跃进行的,因此快速排序是不稳定的
参考文献:
大话数据结构--程杰
文中的动态演示和部分图片选自以下文章:
https://blog.csdn.net/hellozhxy/article/details/79911867#commentsedit
https://www.cnblogs.com/onepixel/articles/7674659.html#!comments