JAVA排序实现总结

前面的话

本次罗列了以下排序实现方式:

1. 冒泡排序
      时间复杂度:O(n^2)
      空间复杂度:O(1)
      稳定性:由于交换的if条件是a[j]>a[j+1],所以如果等于的话并没有交换,所以冒泡排序是一种稳定排序算法。
      冒泡排序较稳定,可用于链式存储结构,时间复杂度较高,当n较大,初始记录无序时,不宜采用此方法

2. 选择排序
      时间复杂度:O(n^2)
      空间复杂度:O(1)
      稳定性:就算法本身来说,是一种稳定排序
      选择排序可用于链式存储结构,移动记录次序较少,当每一记录占用空间较多时,此方法比插入排序快
      
3. 插入排序
      时间复杂度:O(n^2)
      空间复杂度:O(1)
      稳定性:稳定排序
      算法简单稳定,容易实现,也适用于链式存储结构,在单链表中只需修改指针,更适用于初始记录基本有序的情况。对与查找插入位置,我们可以用二分查找获取位置。
      
4. 希尔排序
      时间复杂度:时间复杂度比较复杂,通常认为O(n*log 2  n),当n趋近于无穷大时,可以为O(n(log2 n)^2)
      空间复杂度:O(1)
      稳定性:不稳定
      希尔排序只能用于顺序存储结构,不能用于链式存储结构,增量gap可以有各种取法,但最后一次gap必须等于1, 总比较次数和移动次数较直接插入排序少,当n越大,序列越无序时,效果越明显。
      
5. 归并排序
      时间复杂度:O(nlog2n)
      空间复杂度:O(n)
      稳定性:稳定排序
      归并排序可用于链式结构,且不需要附加存储空间,但递归实现任需要开辟相应的递归工作栈。

6. 快速排序
      时间复杂度:O(nlogn)
      空间复杂度:O(log2n)—O(n)
      稳定性:不稳定
      快速排序过程中需要地位表的上界和下界,所以适合顺序结构,很难用于链式结构,当n非常大时,快速排序是所有排序过程中最快的一种,所以适合用于初始记录无序、n较大时的情况。
      
7. 堆排序
      时间复杂度:O(nlog2 n)
      空间复杂度:O(1)
      稳定性:不稳定
      堆排序只能用于顺序存储结构,初始建堆比较次数较多,记录较少时不宜采用,在最坏情况下较快速排序而言是一个有点,记录较多时较为高效。

冒泡排序基本思想:
两两相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。之所以叫做冒泡排序,是因为每次将一个最大或最小的数往后沉淀,沉淀到后面的数比它大或比它小为止,类似于泡泡向上浮起来的过程。

冒泡排序是排序方法中最直观也是最简单的,我们接触的第一种排序方法也是冒泡排序,基本步骤:

有一个数组a[10],用变量i表示它的下标(i从0开始)
(1) 比较两个相邻元素a[i]和a[i+1],如果a[i]>a[i+1],就交换这两个数的位置;
(2)重复执行第一步,直到比较到最后一对的时候(例:首次是a[8]和a[9],此时,a[9]的值为该数组的最大值,这个值属于有序数列);
(3)对所有元素(除了有序数列里的元素),重复执行第一步和第二步,每执行完一次,都会找到当前比较的数里最大的那个(有序数列就会增加一个);
(4)随着参与比较的元素越来越少,最终没有任何一对元素需要比较的时候,排序完成。


选择排序基本思想:
通过n-1次关键字比较,从n-i+1个记录中选择关键字最小的记录,并和第i个记录交换。

选择排序是经过一次一次在无序区间中找到最值,放到无序区间的前一个位置,基本步骤:

(1)设待排序的记录存放在数组r[n]中,第一趟从r[1]开始,通过n-1次比较,从n个记录中选取最小的记录,记为r[k],交换r[1]和r[k]

(2)第二趟从r[2]开始,通过n-2次比较,从n-1个记录中选出关键字最小的记录,记为r[k],交换r[2]和r[k]

(3)以此类推,第i趟从r[i]开始,通过n-i次比较,从n-i+1个记录中选取最小关键字记录,记为r[k],交换 r[i]和r[k]

(4)经过n-1趟,排序完成


插入排序基本思想:
插入排序(直接插入排序)是一种最简单的排序方法,其基本操作是将一条记录插入到已排号的序列当中,从而得到一个有序的记录。

算法步骤:

(1)设待排序数组a[n],默认a[0]是一个有序序列

(2)循环n-1次,每次将为排序序列插入到前面的已排序序列当中,将已排序序列区间长度加1,未排序区间长度减一

(3)重复(2)直到未排序区间长度为0


希尔排序基本思想:
希尔排序实质上是采用分组插入的方法,先将整个待排序记录序列分成几组,从而减少参与直接插入排序的数据量,对每组分别进行直接插入排序,然后增加每组的数据量,重新分组,这样经过几次分组后,整个序列基本有序,在对全体进行插入排序,希尔排序记录的分组,不是简单的逐段分组,而是将相隔某个记录增量的记录分成一组。

希尔排序的基本步骤:

(1)第一趟取增量gap(gap<n)把全部几记录分成gap个组,对每组进行直接插入排序

(2)第二趟取gap=gap/2,重复第一步

(3)以此类推,直到gap=1,再对整个序列排序一次


归并排序类似与二分查找,都是二分当前区间,分别进行操作,可通过递归与送代进行实现,书面上定义是将两个或两个以上的有序表合成一个有序表的过程,将两个有序表合成一个有序表的过程称为2-路归并,2-路归并最为简单常用。


归并排序的算法思想:
假设初始序列含有n个记录,则可看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并。。。如此重复,直到得到一个长度为n的有序序列为止。

归并排序将r[n]中的记录放到t[n]中,当序列长度等于1时,递归结束,否则:

(1)将当前序列一分为二,求出分裂点mid=(l+r)/2

(2)对子序列r[l]-r[mid]递归,进行归并排序,结果放在t[l,mid]中

(3)对子序列r[mid+1,r]递归,进行归并排序,结果放在t[mid+1,r]

(4)将两个有序的子序列t[l,mid],t[mid+1,r]归并为一个有序的序列放入r[l,r]中


快速排序基本思想:
快速排序在算法竞赛中用到的比较多,快速排序比冒泡排序要快很多,C语言中也有快速排序的函数qsort(),不过最好去理解一下底层代码实现。在冒泡排序过程中,只对两个相邻的记录进行比较,因此每次比较只能消除一个逆序,如果能通过两个(不相邻)记录的一次交换,消除多个逆序,则会大大加快排序的速度,快速排序就是利用这个原理。

基本步骤:

(1)选择待排序记录中第一个记录作为基准,将基准暂存在r[0]的位置上,假设两个指针low和high,初始分别指向表的下界和上界

(2)从表的最右侧位置向左寻找第一个比基准数小的位置,从表的最左侧寻找第一个比基准数大的位置,交换两个元素

(3)重复(2)当low==high时,将基准数放到low位置处的元素判断大于还是小于后,与基准数交换

(4)此时以基准数的位置为分界点,将序列分为两个子序列,分别进行(1),(2),(3)操作,直到序列的长度为1为止


堆排序基本思想:

堆排序是一种树性选择排序,再排序过程中,将排序序列a[n]看成一个完全二叉树,利用二叉树双亲节点与孩子节点的内在联系,在当前无序的序列中,选择的关键字最大(或最小)的记录。

堆的定义为 a[i]>=a[2i]&&a[i]>=a[2i+1] 或 a[i]<=a[2i]&&a[i]<=a[2i+1],通俗的理解就是在完全二叉树中,一个节点必须同时大于它的左节点和右节点。前者为大根堆后者为小根堆。

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得当前无序的序列中选择关键字最大(或最小)的记录变得简单,步骤为:

(1)按照堆的定义将待排序序列a[n] 调整为大根堆,这个过程称为初建堆,交换a[1]和a[n] ,则a[n]为关键字的最大记录

(2)将a[n-1]重新调整为堆,交换ra[1]和a[n-1],此时a[n-1]为关键字最大记录

(3)循环n-1次,直到交换a[1]和a[2]为止

所以实现堆排序需要熟悉两个问题:建初堆、调整堆

调整堆就是比较此节点与它的左右孩子节点,选孩子节点中取最大或者最小的一个,与此节点比较,如果不满足条件就交换,直到进行到叶子节点为止。而要将一个无序序列调整为堆,就必须将其所在的二叉树中以一个节点为根的子树都调整为堆。

0. 通用公共方法

	/**
	 * 比较v元素是否大于w元素
	 */
	private static boolean greater(Comparable v, Comparable w) {
		return v.compareTo(w) > 0;
	}

	/**
	 * 数组元素i和j交换位置
	 */
	private static void exch(Comparable[] a, int i, int j) {
		Comparable temp;
		temp = a[i];
		a[i] = a[j];
		a[j] = temp;
	}
	
	public static Integer[] createRandomIntegerArr(Integer star, Integer end, Integer length) {
		Integer[] arr = new Integer[length];
		Random random = new Random();
		for (int i = 0; i < length; i++) {
			arr[i] = star + random.nextInt(end - star);
		}
		System.out.println("原始随机:" + Arrays.toString(arr));
		return arr;
	}

1. 冒泡排序

要求

  • 能够用自己语言描述冒泡排序算法
  • 能够手写冒泡排序代码
  • 了解一些冒泡排序的优化手段

算法描述

  1. 依次比较数组中相邻两个元素大小,若 a[j] > a[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后
  2. 重复以上步骤,直到整个数组有序

算法实现

public class A1Bubble {
	/**
	 * 对数组a中的元素进行排序
	 * 从左到右,逐一比较,最后把最大的放最右边
	 */
	public static void sort(Comparable[] a) {
		for (int i = a.length - 1; i > 0; i--) {
			for (int j = 0; j < i; j++) {
				//{6,5,4,3,2,1}
				//比较索引j和索引j+1处的值
				if (greater(a[j], a[j + 1])) {
					exch(a, j, j + 1);
				}
			}
		}
	}
}
  • 优化点1:每经过一轮冒泡,内层循环就可以减少一次
  • 优化点2:如果某一轮冒泡没有发生交换,则表示所有数据有序,可以结束外层循环

进一步优化:

public static void bubble_v2(int[] a) {
    int n = a.length - 1;
    while (true) {
        int last = 0; // 表示最后一次交换索引位置
        for (int i = 0; i < n; i++) {
            System.out.println("比较次数" + i);
            if (a[i] > a[i + 1]) {
                Utils.swap(a, i, i + 1);
                last = i;
            }
        }
        n = last;
        System.out.println("第轮冒泡" + Arrays.toString(a));
        if (n == 0) {
            break;
        }
    }
}

2. 选择排序

要求

  • 能够用自己语言描述选择排序算法
  • 能够比较选择排序与冒泡排序
  • 理解非稳定排序与稳定排序

算法描述

  1. 将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集
  2. 重复以上步骤,直到整个数组有序

算法实现

public class A2Selection {
	/**
	 * 对数组a中的元素进行排序
	 * 从左到右找到最小的值,然后获取最小数的索引,最后替换至最左边
	 */
	public static void sort(Comparable[] a) {
		for (int i = 0; i <= a.length - 2; i++) {
			//定义一个变量,记录最小元素所在的索引,默认为参与选择排序的第一个元素所在的位置
			int minIndex = i;
			for (int j = i + 1; j < a.length; j++) {
				//需要比较最小索引minIndex处的值和j索引处的值;
				if (greater(a[minIndex], a[j])) {
					minIndex = j;
				}
			}
			if (i != minIndex) {
				//交换最小元素所在索引minIndex处的值和索引i处的值
				exch(a, i, minIndex);
			}
      System.out.println(Arrays.toString(a));
		}
	}
}
  • 优化点:为减少交换次数,每一轮可以先找最小的索引,在每轮最后再交换元素

与冒泡排序比较

  1. 二者平均时间复杂度都是 O ( n 2 ) O(n^2) O(n2)

  2. 选择排序一般要快于冒泡,因为其交换次数少

  3. 但如果集合有序度高,冒泡优于选择

  4. 冒泡属于稳定排序算法,而选择属于不稳定排序

    • 稳定排序指,按对象中不同字段进行多次排序,不会打乱同值元素的顺序
    • 不稳定排序则反之

3. 插入排序

要求

  • 能够用自己语言描述插入排序算法
  • 能够比较插入排序与选择排序

算法描述

  1. 将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)

  2. 重复以上步骤,直到整个数组有序

算法实现

public class A3Insertion {
	/**
	 *  对数组a中的元素进行排序
	 */
    public static void sort(Comparable[] a) {
        for (int i = 1; i < a.length; i++) {
            
            for (int j = i; j > 0; j--) {
                //比较索引j处的值和索引j-1处的值,如果索引j-1处的值比索引j处的值大,则交换数据,如果不大,那么就找到合适的位置了,退出循环即可;
                if (greater(a[j - 1], a[j])) {
                    exch(a, j - 1, j);
                } else {
                    break;
                }
            }
        }
    }
}

与选择排序比较

  1. 二者平均时间复杂度都是 O ( n 2 ) O(n^2) O(n2)

  2. 大部分情况下,插入都略优于选择

  3. 有序集合插入的时间复杂度为 O ( n ) O(n) O(n)

  4. 插入属于稳定排序算法,而选择属于不稳定排序

提示:

插入排序通常被所轻视,其实它的地位非常重要。小数据量排序,都会优先选择插入排序

4. 希尔排序

要求

  • 能够用自己语言描述希尔排序算法

算法描述

  1. 首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度

  2. 每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二

    ① 少量元素插入排序速度很快

    ② 让组内值较大的元素更快地移动到后方

  3. 当间隙逐渐减少,直至为 1 时,即可完成排序

算法实现

public class A4Shell {
    /*
       对数组a中的元素进行排序
    */
    public static void sort(Comparable[] a) {
        //1.根据数组a的长度,确定增长量h的初始值;
        int h = 1;
        while (h < a.length / 2) {
            h = 2 * h + 1;
        }
        //2.希尔排序
        while (h >= 1) {
            //排序
            //2.1.找到待插入的元素
            for (int i = h; i < a.length; i++) {
                //2.2把待插入的元素插入到有序数列中
                for (int j = i; j >= h; j -= h) {
                    //待插入的元素是a[j],比较a[j]和a[j-h]
                    if (greater(a[j - h], a[j])) {
                        //交换元素
                        exch(a, j - h, j);
                    } else {
                        //待插入元素已经找到了合适的位置,结束循环;
                        break;
                    }
                }
            }
            //减小h的值
            h = h / 2;
        }
    }
}
// 方式二
private static void shell(int[] a) {
    int n = a.length;
    for (int gap = n / 2; gap > 0; gap /= 2) {
        // i 代表待插入元素的索引
        for (int i = gap; i < n; i++) {
            int t = a[i]; // 代表待插入的元素值
            int j = i;
            while (j >= gap) {
                // 每次与上一个间隙为 gap 的元素进行插入排序
                if (t < a[j - gap]) { // j-gap 是上一个元素索引,如果 > t,后移
                    a[j] = a[j - gap];
                    j -= gap;
                } else { // 如果 j-1 已经 <= t, 则 j 就是插入位置
                    break;
                }
            }
            a[j] = t;
            System.out.println(Arrays.toString(a) + " gap:" + gap);
        }
    }
}

5. 归并排序

public class A5Merge {
    //归并所需要的辅助数组
    private static Comparable[] assist;
    /**
     *  比较v元素是否小于w元素
     */
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }
    
    /**
     * 对数组a中的元素进行排序
     */
    public static void sort(Comparable[] a) {
        //1.初始化辅助数组assist;
        assist = new Comparable[a.length];
        //2.定义一个lo变量,和hi变量,分别记录数组中最小的索引和最大的索引;
        int lo = 0;
        int hi = a.length - 1;
        //3.调用sort重载方法完成数组a中,从索引lo到索引hi的元素的排序
        sort(a, lo, hi);
    }
    
    /**
     * 对数组a中从lo到hi的元素进行排序
     */
    private static void sort(Comparable[] a, int lo, int hi) {
        //做安全性校验;
        if (hi <= lo) {
            return;
        }
        
        //对lo到hi之间的数据进行分为两个组
        int mid = lo + (hi - lo) / 2;//   5,9  mid=7
        
        //分别对每一组数据进行排序
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        
        //再把两个组中的数据进行归并
        merge(a, lo, mid, hi);
    }
    
    /**
     * 对数组中,从lo到mid为一组,从mid+1到hi为一组,对这两组数据进行归并
     */
    private static void merge(Comparable[] a, int lo, int mid, int hi) {
        //定义三个指针
        int i = lo;
        int p1 = lo;
        int p2 = mid + 1;
        
        //遍历,移动p1指针和p2指针,比较对应索引处的值,找出小的那个,放到辅助数组的对应索引处
        while (p1 <= mid && p2 <= hi) {
            //比较对应索引处的值
            if (less(a[p1], a[p2])) {
                assist[i++] = a[p1++];
            } else {
                assist[i++] = a[p2++];
            }
        }
        
        //遍历,如果p1的指针没有走完,那么顺序移动p1指针,把对应的元素放到辅助数组的对应索引处
        while (p1 <= mid) {
            assist[i++] = a[p1++];
        }
        //遍历,如果p2的指针没有走完,那么顺序移动p2指针,把对应的元素放到辅助数组的对应索引处
        while (p2 <= hi) {
            assist[i++] = a[p2++];
        }
        //把辅助数组中的元素拷贝到原数组中
        for (int index = lo; index <= hi; index++) {
            a[index] = assist[index];
        }
    }
}

6. 快速排序

要求

  • 能够用自己语言描述快速排序算法
  • 掌握手写单边循环、双边循环代码之一
  • 能够说明快排特点
  • 了解洛穆托与霍尔两种分区方案的性能比较

算法描述

  1. 每一轮排序选择一个基准点(pivot)进行分区
    1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
    2. 当分区完成时,基准点元素的位置就是其最终位置
  2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer
  3. 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案

单边循环快排(lomuto 洛穆托分区方案)

  1. 选择最右元素作为基准点元素
  2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换
  3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引
  4. 最后基准点与 i 交换,i 即为分区位置
public static void quick(int[] a, int l, int h) {
    if (l >= h) {
        return;
    }
    int p = partition(a, l, h); // p 索引值
    quick(a, l, p - 1); // 左边分区的范围确定
    quick(a, p + 1, h); // 左边分区的范围确定
}

private static int partition(int[] a, int l, int h) {
    int pv = a[h]; // 基准点元素
    int i = l;
    for (int j = l; j < h; j++) {
        if (a[j] < pv) {
            if (i != j) {
                swap(a, i, j);
            }
            i++;
        }
    }
    if (i != h) {
        swap(a, h, i);
    }
    System.out.println(Arrays.toString(a) + " i=" + i);
    // 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
    return i;
}

双边循环快排(不完全等价于 hoare 霍尔分区方案)

  1. 选择最左元素作为基准点元素
  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
  3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

要点

  1. 基准点在左边,并且要先 j 后 i
  2. while( i < j && a[j] > pv ) j–
  3. while ( i < j && a[i] <= pv ) i++
private static void quick(int[] a, int l, int h) {
    if (l >= h) {
        return;
    }
    int p = partition(a, l, h);
    quick(a, l, p - 1);
    quick(a, p + 1, h);
}

private static int partition(int[] a, int l, int h) {
    int pv = a[l];
    int i = l;
    int j = h;
    while (i < j) {
        // j 从右找小的
        while (i < j && a[j] > pv) {
            j--;
        }
        // i 从左找大的
        while (i < j && a[i] <= pv) {
            i++;
        }
        swap(a, i, j);
    }
    swap(a, l, j);
    System.out.println(Arrays.toString(a) + " j=" + j);
    return j;
}

快排特点

  1. 平均时间复杂度是 O ( n l o g 2 ⁡ n ) O(nlog_2⁡n ) O(nlog2n),最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)

  2. 数据量较大时,优势非常明显

  3. 属于不稳定排序

洛穆托分区方案 vs 霍尔分区方案

  • 霍尔的移动次数平均来讲比洛穆托少3倍
public class A6Quick {
    /**
     * 比较v元素是否小于w元素
     */
    private static boolean less(Comparable v, Comparable w) {
        return v.compareTo(w) < 0;
    }
    
    //对数组内的元素进行排序
    public static void sort(Comparable[] a) {
        int lo = 0;
        int hi = a.length - 1;
        sort(a, lo, hi);
    }
    
    //对数组a中从索引lo到索引hi之间的元素进行排序
    private static void sort(Comparable[] a, int lo, int hi) {
        //安全性校验
        if (hi <= lo) {
            return;
        }
        //需要对数组中lo索引到hi索引处的元素进行分组(左子组和右子组);
        int partition = partition(a, lo, hi);//返回的是分组的分界值所在的索引,分界值位置变换后的索引
        
        //让左子组有序
        sort(a, lo, partition - 1);
        //让右子组有序
        sort(a, partition + 1, hi);
    }
    
    //对数组a中,从索引 lo到索引 hi之间的元素进行分组,并返回分组界限对应的索引
    public static int partition(Comparable[] a, int lo, int hi) {
        //确定分界值
        Comparable key = a[lo];
        //定义两个指针,分别指向待切分元素的最小索引处和最大索引处的下一个位置
        int left = lo;
        int right = hi + 1;
        
        //切分
        while (true) {
            //先从右往左扫描,移动right指针,找到一个比分界值小的元素,停止
            while (less(key, a[--right])) {
                if (right == lo) {
                    break;
                }
            }
            //再从左往右扫描,移动left指针,找到一个比分界值大的元素,停止
            while (less(a[++left], key)) {
                if (left == hi) {
                    break;
                }
            }
            //判断 left>=right,如果是,则证明元素扫描完毕,结束循环,如果不是,则交换元素即可
            if (left >= right) {
                break;
            } else {
                exch(a, left, right);
            }
        }
        //交换分界值
        exch(a, lo, right);
        return right;
    }
}

7. 堆排序

堆排序是一种树性选择排序,再排序过程中,将排序序列a[n]看成一个完全二叉树,利用二叉树双亲节点与孩子节点的内在联系,在当前无序的序列中,选择的关键字最大(或最小)的记录。

堆的定义为 a[i]>=a[2i]&&a[i]>=a[2i+1] 或 a[i]<=a[2i]&&a[i]<=a[2i+1],通俗的理解就是在完全二叉树中,一个节点必须同时大于它的左节点和右节点。前者为大根堆后者为小根堆。

堆排序利用了大根堆(或小根堆)堆顶记录的关键字最大(或最小)这一特征,使得当前无序的序列中选择关键字最大(或最小)的记录变得简单,步骤为:

(1)按照堆的定义将待排序序列a[n] 调整为大根堆,这个过程称为初建堆,交换a[1]和a[n] ,则a[n]为关键字的最大记录

(2)将a[n-1]重新调整为堆,交换ra[1]和a[n-1],此时a[n-1]为关键字最大记录

(3)循环n-1次,直到交换a[1]和a[2]为止

所以实现堆排序需要熟悉两个问题:建初堆、调整堆

调整堆就是比较此节点与它的左右孩子节点,选孩子节点中取最大或者最小的一个,与此节点比较,如果不满足条件就交换,直到进行到叶子节点为止。而要将一个无序序列调整为堆,就必须将其所在的二叉树中以一个节点为根的子树都调整为堆。

时间复杂度:O(nlog2 n)

空间复杂度:O(1)

稳定性:不稳定

堆排序只能用于顺序存储结构,初始建堆比较次数较多,记录较少时不宜采用,在最坏情况下较快速排序而言是一个有点,记录较多时较为高效。

public class HeapSort {
    
    private static void heap(int[] a) {
        int count = a.length;
        heapify(a, count);
        System.out.println(Arrays.toString(a));
        int end = count - 1;
        while (end > 0) {
            swap(a, end, 0);
            end--;
            siftDown(a, 0, end);
        }
    }
    
    private static void heapify(int[] a, int count) {
        int start = getParent(count - 1);
        System.out.println(Arrays.toString(a));
        while (start >= 0) {
            siftDown(a, start, count - 1);
            System.out.println(Arrays.toString(a));
            start--;
        }
    }
    
    private static void siftDown(int[] a, int start, int end) {
        int root = start;
        while (getLeftChild(root) <= end) {
            int child = getLeftChild(root);
            int swap = root;
            if (a[swap] < a[child]) {
                swap = child;
            }
            if (child + 1 <= end && a[swap] < a[child + 1]) {
                swap = child + 1;
            }
            if (swap == root) {
                return;
            } else {
                swap(a, root, swap);
                root = swap;
            }
        }
    }
    
    private static int getParent(int i) {
        return (i - 1) / 2;
    }
    
    private static int getLeftChild(int i) {
        return 2 * i + 1;
    }
    
    private static int getRightChild(int i) {
        return 2 * i + 2;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值