1. 排序算法模板
public abstract class SortTemplate { //排序算法,子类实现 public abstract void sort(Comparable[] a); //判断a是否小于b,小于0为true public static boolean less(Comparable a, Comparable b) { return a.compareTo(b) < 0; } //第i个和第j个交换 public static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } public static void show(Comparable []a){ for (int i = 0; i < a.length; i++) { System.out.print(a[i]+" "); } } //判断数组是否有序 public static boolean isSorted(Comparable []a){ for (int i = 1; i < a.length; i++) { if (less(a[i],a[i-1])){ return false; } } return true; } }
1.1 选择排序
找到数组中最小的元素,然后和数组第一个元素交换;接下来再从剩下元素中找到最小的元素和数组第二个元素交换,依次类推下去,直到数组最后一个元素就结束。
class SelectSort extends SortTemplate { @Override public void sort(Comparable[] a) { for (int i = 0; i < a.length; i++) { int min = i; for (int j = i + 1; j < a.length; j++) { if (less(a[j], a[i])) { min = j; } exch(a, i, min); } } } }
1.2 插入排序
从数组的第二个元素开始与它前面的元素依次比较,如果小于就交换,直至它前面没有元素就进行下一轮,从数组的第三个元素开始依次类推,到数组的最后一个元素比较交换完就结束。
class InsertSort extends SortTemplate {
@Override
public void sort(Comparable[] a) {
for (int i = 1; i < a.length; i++) {
for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) {
exch(a, j, j - 1);
}
}
}
}小结:
①插入排序:1.不会访问索引右侧元素;
②选择排序:1.不会访问索引左侧元素;
1.3 希尔排序
为了解决在大规模下插入排序很慢的问题,一种基于插入排序的希尔排序,它解决插入排序只与相邻的元素交换问题,采用交换不相邻元素对数组的局部进行插入排序,并最终使用一次插入排序将局部有序的数组进行排序即可。
class ShellSort extends SortTemplate { //对数组进行多次分组,从大分组开始对每小组进行插入排序, //则至分组为1时,再最后一次进行插入排序即可 @Override public void sort(Comparable[] a) { int h = 1; //对数组进行分组(递增序列),进行局部插入排序 while (h < a.length / 3) { h = 3 * h + 1;//1,4,13,40,121,364,1093,... } while (h >= 1) { for (int i = h; i < a.length; i++) { for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) { exch(a, j, j - h); } } h = h / 3; } } }小结:比起插入和选择排序,希尔排序更适合处理大规模数组排序。
1.4 归并排序
归并排序会先递归地将数组分为两半进行分别排序,然后将结果归并起来。时间复杂度为NlogN,缺点所需的额外空间和数组长度成正比。分而治之思想。
1.4.1 原地归并的抽象方法
class MergeSort extends SortTemplate {
public static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
Comparable[] aux = new Comparable[hi - lo];
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(aux[j], aux[i])) {
//右半边的当前元素小于左半边的当前元素,取右半边的元素
a[k] = aux[j++];
} else {
//右半边的当前元素大于左半边的当前元素,取左半边的元素
a[k] = aux[i++];
}
}
}
@Override
public void sort(Comparable[] a) {}
}1.4.2 自顶向下的归并排序
class TopToDownMergeSort extends MergeSort{
public static Comparable[] aux;//归并所需的辅助数组
@Override
public void sort(Comparable[] a) {
aux = new Comparable[a.length];
topToDownSort(a, 0, a.length - 1);
}
public static void topToDownSort(Comparable[] a, int lo, int hi) {
//将数组a[lo..hi]排序
if (hi <= lo) {
return;
}
int mid = lo + (hi - lo) / 2;
topToDownSort(a, lo, mid);//将左半边排序
topToDownSort(a, mid + 1, hi);//将右半边排序
merge(a, lo, mid, hi);//归并结果
}
}
} 不难发现在递归里面会使小规模问题中的方法调用过于频繁,可以使用插入排序处理小规模的子数组,可将归并排序的时间缩减10%—15%。同样可以添加一个条件当a[mid]小于等于a[mid+1]时,就可认为该数组已经有序了,可跳过merge()方法。1.4.3 自底向上的归并排序
class DownToTopMergeSort extends MergeSort {
public static Comparable[] aux;//归并所需的辅助数组
@Override
public void sort(Comparable[] a) {
aux = new Comparable[a.length];
for (int i = 1; i < a.length; i = i + i) {//i子数组大小
for (int j = 0; j < a.length - i; j += (i + i)) {//j子数组索引
merge(a, j, j + i - 1, Math.min(j + i + i - 1, a.length - 1));
}
}
}
}小结:有且仅当当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和 数组访问次数是相同的。
①自底向上的归并排序比较适合用链表组织的数据,只需要重新组织链表链接就能将链表原地排序,不需要创建任何新的链表结点。
1.5 快速排序
1.5.1 二分快速排序(一般快速排序)
实现简单、最流行、原地排序、时间复杂度NlogN,当原数组有序则退化成冒泡排序时间复杂度N²。
每一轮拿数组或子数组的第一个元素作为基准数,定义两个指针,左指针找到一个大于基准数的元素,右指针找到一个小于基准数的元素,然后交换这两个元素,直到两个指针指向同一个元素时,将基准数与这个元素交换,则此轮排序结束,这个基准数就排好序了。直到子数组剩一个元素则整个数组就排好序了,就可以结束排序了。
class QuickSort extends SortTemplate {
@Override
public void sort(Comparable[] a) {
//打乱数组顺序,防止时间复杂度为n²
Collections.shuffle(Arrays.asList(a));
quickSort(a, 0, a.length - 1);
}
public static void quickSort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int j = partition(a, lo, hi);//切分
quickSort(a, lo, j - 1);//将左半边排序
quickSort(a, j + 1, hi);//将右半边排序
}
public static int partition(Comparable[] a, int lo, int hi) {
int i = lo, j = hi + 1;//左右指针
Comparable comparable = a[lo];//切分元素(基准元素)
while (true) {//左右扫描是否结束并交换元素
while (less(a[++i], comparable)) {
if (i == hi) {
break;
}
}
while (less(comparable, a[--j])) {
if (j == lo) {
break;
}
}
if (i >= j) {
break;
}
exch(a, i, j);
}
exch(a, lo, j);//将基准元素放入正确位置
return j;
}
}为了优化快速排序的性能,①我们可以在排序小数组的时候切换成插入排序。代码如下:将 if (hi <= lo) { return; } 改为 if (hi <= lo+M) { InsertSort.sort(a,lo,hi); return; } ,M的值决定是多大的数组。一般是5-15之间。②采用三向切分:将数组切分为三份,小于等于和大于切分元素(默认第一个元素)三份,三向切分的快速排序适合存在大量重复元素的数组,时间复杂度从线性对数级别变成线性级别。
1.5.2 三切切分的快速排序
时间复杂度为N,适合存在大量重复的数组排序。
class ThreeWayQuickSort extends SortTemplate {
public void threeWayQuicksort(Comparable[] a, int lo, int hi) {
if (hi <= lo) {
return;
}
int lt = lo, i = lo + 1, gt = hi;
Comparable v = a[lo];
while (i <= gt) {
int cmp = a[i].compareTo(v);
if (cmp < 0) {
exch(a, lt++, i++);
} else if (cmp > 0) {
exch(a, i, gt--);
} else {
i++;
}
}
threeWayQuicksort(a, lo, lt - 1);
threeWayQuicksort(a, gt + 1, hi);
}
@Override
public void sort(Comparable[] a) {}
}
1.6 优先队列
优先队列是一种支持删除最大元素和插入元素的数据类型。它适合那些按照时间顺序来处理所有事件、任务调度(任务优先级高先执行)等场景。
1.6.1 基于(二叉堆)堆排序的优先队列
①由下至上的堆有序化(上浮)-->数组末尾插入元素
当某一结点比它的父结点大时,这个结点需要与它的父结点交换,交换后,该结点可能还比它现在的父结点大,那就继续与父节点进行交换。通过不断向上移动,直到遇到一个更大父结点才结束交换,堆才有序。
//上浮 public void swim(int k) { while (k > 1 && less(k / 2, k)) { exch(k / 2, k); k = k / 2; } }②由上至下的堆有序化(下沉)-->删除最大元素(将数组末尾元素放到原最大元素的位置)
当某一结点比它的两个子结点的其中之一小时,这个结点需要与它的大的子结点做交换,交换后,该结点可能还比它现在的两个子结点的其中之一小,那就继续与大结点进行交换。通过不断向下移动,直到它的子结点都比它小时才结束交换,堆才有序。
//下沉
public void sink(int k) {
while (2 * k <= pq.length) {
int j = 2 * k;
if (j < pq.length && less(j, j + 1)) {
j++;
}
if (!less(k, j)) {
break;
}
exch(k, j);
k = j;
}
}
1. 基于(大顶堆)堆的优先队列算法实现:class BigHeapSort { private Comparable[] pq; private void HeapSort(int n) { pq = new Comparable[n]; } //i < j为true public boolean less(int i, int j) { return pq[i].compareTo(pq[j]) < 0; } public void exch(int i, int j) { Comparable t = pq[i]; pq[i] = pq[j]; pq[j] = t; } //上浮 public void swim(int k) { while (k > 1 && less(k / 2, k)) { exch(k / 2, k); k = k / 2; } } //下沉 public void sink(int k) { while (2 * k <= pq.length) { int j = 2 * k; if (j < pq.length && less(j, j + 1)) { j++; } if (!less(k, j)) { break; } exch(k, j); k = j; } } //插入元素 public void insert(Comparable v) { int n = pq.length + 1; pq[n] = v; sink(n);//上浮 } //删除最大元素 public Comparable delMax() { Comparable comparable = pq[1];//大顶堆得到最大元素 exch(1, pq.length);//将其和最后一个结点交换 pq[pq.length] = null;//防止对象游离 sink(1);//下沉 return comparable; } } 时间复杂度为logN对数级别。2. 索引优先队列:
//大顶堆
public class MaxHeapIndexPriorityQueue<T> {
private List<Integer> pq;
private List<Comparable> elements;
private Comparator<T> comparator;
public MaxHeapIndexPriorityQueue(Comparator<T> comparator) {
this.pq = new ArrayList<Integer>();
this.elements = new ArrayList<>();
this.comparator = comparator;
}
public void insert(int index, T element) {
pq.add(index);
elements.add(index, (Comparable) element);
swim(pq.size() - 1);
}
public void delete(int index) {
int indexInPq = pq.indexOf(index);
exch(indexInPq, pq.size() - 1);
pq.remove(pq.size() - 1);
elements.remove(index);
sink(indexInPq);
}
public void update(int index, T element) {
elements.set(index, (Comparable) element);
int indexInPq = pq.indexOf(index);
swim(indexInPq);
sink(indexInPq);
}
public boolean isEmpty() {
return pq.isEmpty();
}
public int size() {
return pq.size();
}
public T getMaximum() {
int maxIndex = pq.get(0);
return (T) elements.get(maxIndex);
}
public int getMaxIndex() {
return pq.get(0);
}
public T deleteMaximum() {
int maxIndex = pq.get(0);
exch(0, pq.size() - 1);
pq.remove(pq.size() - 1);
sink(0);
return (T) elements.remove(maxIndex);
}
private void swim(int k) {
while (k > 0 && less(k / 2, k)) {
exch(k / 2, k);
k = k / 2;
}
}
private void sink(int k) {
while (2 * k <= pq.size() - 1) {
int j = 2 * k;
if (j < pq.size() - 1 && less(j, j + 1)) {
j++;
}
if (!less(k, j)) {
break;
}
exch(k, j);
k = j;
}
}
private boolean less(int i, int j) {
return elements.get(pq.get(i)).compareTo(elements.get(pq.get(j))) < 0;
}
private void exch(int i, int j) {
int temp = pq.get(i);
pq.set(i, pq.get(j));
pq.set(j, temp);
}
}
3. 通过索引优先队列来实现优先队列的多向归并:解决输入多行字符串,输出所有字符串的字母排序
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
strings.add("adada");
strings.add("bcedada");
strings.add("adrtada");
for (String string : strings) {
MaxHeapIndexPriorityQueue<String> indexPriorityQueue = new MaxHeapIndexPriorityQueue<String>(string.length());
for (int i = 0; i < string.length(); i++) {
indexPriorityQueue.insert(i, String.valueOf(string.charAt(i)));
}
while (!indexPriorityQueue.isEmpty()){
System.out.println(indexPriorityQueue.getMaximum());
String s = indexPriorityQueue.deleteMaximum();
}
}
}
1.7 堆排序
将所有元素插入一个查找最小元素的优先队列,然后再重复调用用删除最小元素的操作来将它们按顺序删去。基于无序数组实现的优先队列就变成了选择排序,基于堆的优先队列就变成了堆排序。
堆排序,在堆构造阶段,将原始数组重新组织安排进一个堆中,然后在下沉排序阶段,我们从堆中按递减顺序取出所有元素并得到排序结果。下面使用一个面向最大元素的优先队列并重复删除最大元素。
class HeapSort extends SortTemplate {
@Override
public void sort(Comparable[] a) {
int N = a.length;
//构造推-->大顶推
for (int i = N / 2; i > 0; i--) {
sink(a, N, i);
}
while (N > 1) {
exch(a, N, 1);
sink(a, N, 1);
}
}
private static void sink(Comparable[] arr, int n, int i) {
int largest = i; // 初始化根节点为最大值
int left = 2 * i + 1; // 左子节点的索引
int right = 2 * i + 2; // 右子节点的索引
// 如果左子节点比根节点大,则更新最大值节点
if (left < n && !less(arr[left], arr[largest])) {
largest = left;
}
// 如果右子节点比最大值节点大,则更新最大值节点
if (right < n && !less(arr[right], arr[largest])) {
largest = right;
}
// 如果最大值节点不是根节点,则交换根节点和最大值节点,并递归调整交换后的子树
if (largest != i) {
Comparable swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
sink(arr, n, largest);
}
}
}先下沉后上浮的堆优化:
class ImprovedHeapSort extends SortTemplate { @Override public void sort(Comparable[] a) { int N = a.length; // 建堆,使用Floyd的下沉后上浮操作 for (int i = N / 2; i > 0; i--){ siftdown(a, N, i); } // 逐个取出堆顶元素并重新调整堆 while (N > 1) { // 把当前根节点(最大值)放到数组末尾 exch(a, N, 1); // 调整堆,使剩下的元素满足堆的性质 siftdown(a, N, 1); } } private static void siftdown(Comparable[] arr, int n, int i) { int largest = i; // 初始化根节点为最大值 int left = 2 * i + 1; // 左子节点的索引 int right = 2 * i + 2; // 右子节点的索引 // 如果左子节点比根节点大,则更新最大值节点 if (left < n && !less(arr[left], arr[largest])) { largest = left; } // 如果右子节点比最大值节点大,则更新最大值节点 if (right < n && !less(arr[right], arr[largest])) { largest = right; } // 如果最大值节点不是根节点,则交换根节点最大值节点,并递归调整交换后的子树 if (largest != i) { exch(arr, i, largest); siftdown(arr, n, largest); } else { // 若根节点没有发生交换,进行上浮操作 siftup(arr, i); } } private static void siftup(Comparable[] arr, int i) { while (i > 0 && !less(arr[i], arr[(i - 1) / 2])) { int parent = (i - 1) / 2; exch(arr, parent, i); i = parent; } } }注:经过优化的堆排序,插入操作和删除最大元素操作的时间复杂度也为logN对数级别,适合在将字符串或其他键值较长的类型的元素的排序,虽然性能得到了提升,但需要更多额外的空间来存储上浮元素。在大规模数据面前堆排序都不适合,请选择其他高效排序算法。
构造大顶堆和小顶堆的选择:①找出多个大元素则用小顶堆(与第一个元素比较),反之用大顶堆;②找出单个大元素则用大顶推(第一个元素为最大元素),反之用小顶推。
1.8 算法总结
①稳定性算法:该算法能够保证相等元素的顺序在排序前后保持不变。
如:插入排序、冒泡排序、归并排序、基数排序、计数排序、桶排序。
不稳定算法,如选择排序、快速排序、堆排序、希尔排序。
②快速排序是最佳选择,如果稳定性很重要而且空间也不是问题,则归并排序可能是最好的。在Java的JDK中对原始数据类型选择三向切分的快速排序,对引用类型使用归并排序