Table of Contents
待排序数组为a[],定义基本操作,比较两个元素大小
private boolean less(int i, int j) {
return i < j;
}
交换指定位置数组两个元素
private void exch(int[] a, int j, int i) {
int tmp = a[j];
a[j] = a[i];
a[i] = tmp;
}
1.选择排序
外层进行N次循环,第i次循环实现将a[i:n-1]中最小值放置到a[i]位置处。
public void SelectionSort(int a[]){
for(int i=0; i<a.length; i++){
int min = i;
//将a[i:n-1]中最小值放置在a[i]位置处
for(int j=i+1; j<a.length; j++){
if(less(a[j], a[min]))
min = j;
}
}
}
2.插入排序
外层N次循环,第i次实现将a[i]插入到a[0:i-1]合适位置处。
public void InsertionSort(int a[]){
for(int i=1; i<a.length; i++){
//将a[i]放到a[0:i-1]合适位置处
for(int j=i; j>0 && less(a[j], a[j-1]); j--){
exch(a, j, j-1);
}
}
}
插入排序示意如下:
3.冒泡排序
外层N次循环,第i次实现将a[0:n-1-i]中最大值放到a[n-1-i]位置处。
public void BubbleSort(int[] a){
for(int i=0; i<a.length; i++){
//将a[j:n-1-i]中最大值移动到a[n-1-i]位置处
for(int j=0; j<a.length-1-i; j++){
if(!less(a[j],a[j+1])){
exch(a, j, j+1);
}
}
}
}
4.希尔排序
依据递增数列1,4,13...依次实现数组h有序,即每次外层循环结束,数组时h有序的,任意间隔h的元素都是有序的;减小h的值,直到h=1此时数组是全部有序的。对于每一次h排序,类似于插入排序,实现a[i] a[i-h] a[i-2h]...有序。
public void ShellSort(int[] a){
int N = a.length;
int h = 1;
while(h < N/3){
h = h * 3 + 1;
}
while(h>=1){
for(int i=h; i<N; i++){
//将a[i]放置在a[i-h],a[i-2h]...合适位置处
for(int j=i; j>=h && less(a[j], a[j-h]); j=j-h){
exch(a, j, j-h);
}
}
h = h /3;
}
}
希尔排序示意如下:
5.归并排序
将数组分为左右两部分,并且两部分都是有序的,然后合并左右两部分实现整体有序;对于左右两部分继续划分直到左右两部分都是单个元素,此时可以直接执行归并操作;
两个有序子数组的归并操作 a[lo:mid] 有序 a[mid+1:hi]有序 合并两个数组,使得整体有序 。
private void merge(int[] a, int lo, int mid, int hi) {
int i = lo;
int j = mid + 1;
for(int k=lo; k<=hi; k++){
aux[k] = a[k];
}
for(int k=lo; k<=hi; k++){
if(i>mid) a[k] = aux[j++];
else if(j>hi) a[k] = aux[i++];
else if(less(a[i], a[j])) a[k] = aux[i++];
else a[k] = aux[j++];
}
}
自上到下的归并排序 利用递归不断划分,然后执行归并操作。
//自顶向下归并排序
public void MergeSort(int[] a){
aux = new int[a.length];
mergesort(a, 0, a.length-1);
}
private void mergesort(int[] a, int lo, int hi) {
//只有一个元素时直接返回
if(hi<=lo) return;
int mid = (lo + hi) >> 1;
//a[lo:mid]有序
mergesort(a, lo, mid);
//a[mid+1:hi]有序
mergesort(a, mid+1, hi);
//将a[lo:mid]和a[mid+1:hi]有序合并
merge(a, lo, mid, hi);
}
实现示意如下:
自下到上归并排序 首先将相邻两元素归并有序,依次4元素归并,8元素,... 直到整体有序。
//自底向上归并排序
public void MergeSort2(int[] a){
aux = new int[a.length];
int N = a.length;
for(int sz=1; sz<N; sz=sz+sz){
for(int lo=0; lo<N-sz; lo+=sz+sz){
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
}
}
6.快速排序
将数组切分给定某一位置,使得位置之前元素小于该位置,之后元素大于该位置。然后递归处理之前以及之后两个子数组。
数组切分的实现,给定数组a[lo:hi]处理数组返回q,使得a[lo:q-1]<=a[q]<=a[q+1:hi]
实现1
private int partition(int[] a, int lo, int hi) {
int i = lo;
int j = hi+1;
//以a[lo]作为基准来比较
int v = a[lo];
while(true){
//从左边找到第一个大于v的数
while(less(a[++i], v)) if(i==hi) break;
//从右边找到第一个小于v的数
while(less(v, a[--j])) if(j==lo) break;
if(i>=j) break;
exch(a, i, j);
}
return i;
}
实现2
private int partition2(int[] a, int lo, int hi){
//以a[hi]作为基准
int x = a[hi];
int i = lo-1;
for(int j=lo; j<hi; j++){
if(less(a[j], x)){
exch(a, i++, j);
}
}
exch(a, i+1, hi);
return i+1;
}
快速排序的实现
//快速排序
public void QuickSort(int[] a){
quicksort(a, 0, a.length-1);
}
private void quicksort(int[] a, int lo, int hi) {
//子数组只有一个元素不需要切分直接返回
if(hi<=lo) return;
//a[lo:q-1]<=a[q]<=a[q+1:hi]
int q = partition(a, lo, hi);
quicksort(a, lo, q-1);
quicksort(a, q+1, hi);
}
快速排序过程显示如下:
三向切分快速排序实现 解决大量重复子数组的切分问题
//三向切分的快速排序
public void Quick3way(int[] a){
quick3way(a, 0, a.length-1);
}
private void quick3way(int[] a, int lo, int hi) {
if(hi<=lo) return;
int lt = lo;
int i = lo+1;
int gt = hi;
int v = a[lo];
while(i <= gt){
//如果a[i]<v,则交换lt与i位置元素,此时i位置=v,则i可以++
if(a[i] < v) exch(a, lt++, i++);
//如果a[i]>v,交换i与gt位置,此时i位置=a[gt]未知与v的大小关系,因此不能++,但是此时gt位置>v,因此gt可以--
else if(a[i] > v) exch(a, i, gt--);
else i++;
}
quick3way(a, lo, lt-1);
quick3way(a, gt+1, hi);
}
三向切分快速排序显示如下:
7.堆排序
最大堆的定义 是一颗二叉树,对于每一个节点满足:根节点值大于左右子节点值。
假定堆使用数组,按照二叉树的层序顺序来存储。如果数组0位置不使用,则从1,2,...n,对于某一个节点k,其父节点为k/2,其左右孩子节点为2k以及2k+1;如果使用0元素位置,则有对于某一个节点k,其父节点为k/2,其左右孩子节点为2k+1以及2k+2;
堆的下沉调整,如果某个位置不满足最大堆的定义,可以将该位置元素与左右子节点最大值元素进行调换,再调整左右子节点变换节点直到满足最大堆性质。
堆排序实现思路:
将给定的数组看成一个最大堆,首先对于0-N/2位置元素进行下沉操作,则可以构造一个最大堆,此时堆顶元素为数组元素最大值;
将堆顶元素与数组最后一个元素交换,此时堆的元素个数减一,堆顶元素进行下沉操作,此时堆顶为第二大元素,依次直到所有元素有序;
//堆排序
public void HeapSort(int[] a){
int N = a.length-1;
//建立一个最大堆
for(int i=N/2; i>=0; i--){
sink(a, i, N);
}
//将堆顶元素取出放到数组最后位置,然后将堆顶元素下沉
while(N>0){
exch(a, 0, N--);
sink(a, 0, N);
}
}
//下沉操作,将a[k]与子节点比较,放到合适位置,维持最大堆的性质 a[k]>a[2k+1]&&a[k]>a[2k+2]
private void sink(int[] a, int k, int N) {
while(2*k+1<=N){
int j = 2*k+1;
if(j+1<=N && a[j]<a[j+1]) j++;
if(a[k]>a[j]) break;
exch(a, j, k);
j = k;
}
}
堆排序示意如下:
8.排序方式比较
1. 稳定性
如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。
这个性质在有些场景中是必要的,特别是我们要对数据集进行多轮排序时。比如我们要排序的是交易事务数据集,每个交易事务都有交易时间和交易金额等信息。我们第一轮先按照交易金额排序,然后我们想再对于这些交易事务按照交易时间排一次序。此时若排序算法是稳定的,上一步具有相同交易时间的事务在第二轮排序后的相对顺序是不变的,而若算法不稳定第二轮对交易时间的排序会破坏第一轮排序的成果。显然我们在这种情况下更希望排序算法是稳定的。
稳定的排序算法有冒泡排序、插入排序和归并排序,而选择排序、希尔排序、快速排序和堆排序都是不稳定的。
2. 原地排序
原地排序指的是对待排数组进行排序时只需在原数组处来回移动数组元素来实现排序。
原地排序的算法有:选择排序、插入排序、希尔排序、快速排序与堆排序;非原地排序算法只有归并排序。
3.时间复杂度以及空间复杂度