基本排序算法

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中对原始数据类型选择三向切分的快速排序,对引用类型使用归并排序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值