算法-排序-学习笔记

使用Java对排序算法学习和练习
代码标准参考:http://www.cyc2018.xyz/
# 算法-排序
    待排序的数组需要实现JavaComparable接口,该接口有CompareTo()方法,用来判断两个元素的大小关系
    使用辅助函数less()swap() 来进行比较和交换的操作,使代码可读性更好。
    排序算法的成本模型是 比较和交换的次数
    
    public abstract class Sort<T extends Comparable<T>>{
        public abstract void sort(T[] nums);
        
        protected boolean less(T v, T w){
            // x,compareTo(y) 小于则返回-1, 大于则返回1,相等则0 
            return v.comparaTe(w) < 0; //确定返回小于,则true, 大于则false
        }

        protected void swap(T[] a, int i , int j){
            T t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }



1. 选择排序:从数组中选择最小元素,将它与数组的第一个元素交换位置。再将数组剩下的元素中选择最小的元素依次交换位置。
            选择排序需要 N2次比较 和 N次交换,运行与输入无关。
   
   public class Selection<T extends Comparable<T>> extends Sort<T>{

        @Override
        public void sort(T[] nums){
            int N = nums.length;
            for(int i = 0; i < N-1; i++){
                int min = i;
                for(int j = i+1; j < N; j++){
                    if(less(nums[j], nums[min]){
                        min = j;
                    }
                }
                swap(nums, i , min);
            }
        }
   }

2. 冒泡排序: 从左到右不断交换相邻逆序的元素。在一轮循环之后,可以让未排序的最大元素上浮到右侧。
            如果在一轮循环中,如果没有发生交换,说明说组已经有序,可以直接退出
    
    public class Bubble<T extends Comparable<T>> extends Sort<T>{
        @Override
        public void sort(T[] nums){
            int N = nums.length;
            boolean isSorted = false;
            for(int i = N - 1; i > 0 && !isSorted; i-- ){
                // 每一轮循环先置true。且每一轮循环会少一位,因为有一个最值到最右端了
                isSorted = true;
                for(int j = 0; j < i; j++){
                    // 这一轮循环会找到一个最大值到最右端
                    if(less(nums[j+1], nums[j])){
                        // 如果j+1 小于 j,则交换两个的位置 
                        isSorted = false;
                        swap(nums, j, j+1);
                    }
                }
            }
        }
    }

3. 插入排序: 每次都将当前元素插入到左侧已经排序的数组中,使得插入后左侧数组依然有序。
            对于数组存在逆序,插入排序每次只能交换相邻元素,令逆序数量减少1,因此插入排序需要交换的次数为逆序数量
            插入排序的时间复杂度取决于数组的初始顺序,部分有序则逆序少,交换次数少,时间复杂度少。
            平均需要 N2 /4 比较和 交换 。 最坏需要 N2 / 2比较和交换,逆序排序。  最好是 N- 1次比较和0次交换。
            public class Insertion<T extendx Comparable<T>> extends Sort<T>{
                @Override
                public void sort(T[] nums){
                    int N = nums.length;
                    for(int i = 1; i < N; i++){
                        // 从左侧开始,j > 0 且后面小于前面则交换
                        for(int j = i; j > 0 && less(nums[j], nums[j -1]; j--)){
                            swap(nums, j, j-1)
                        }

                    }
                }
            }

4. 希尔排序: 对于大规模的数组,插入排序很慢,因为只能交换相邻的元素,每次只能将逆序数量-1. 希尔排序就是为了解决这种局限性,通过交换不相邻的元素, 使逆序数量减少大于1
            使用插入排序对间隔h的序列进行排序。通过不断减小h,最后令h=1,可以使数组有序
            public class Shell<T extends Comparable<T>> extends Sort<T> {

    @Override
    public void sort(T[] nums) {

        int N = nums.length;
        int h = 1;
		// 比插入排序多了这一步,其他一致。
        while (h < N / 3) {
            h = 3 * h + 1; // 1, 4, 13, 40, ...
        }

        while (h >= 1) {
            for (int i = h; i < N; i++) {
                for (int j = i; j >= h && less(nums[j], nums[j - h]); j -= h) {
                    swap(nums, j, j - h);
                }
            }
            h = h / 3;
        }
    }
}


5. 归并方法: 归并方法将数组中两个已经排序的部分归并成一个
   public abstract class MergeSort<T extends Comparable<T> extends Sort<T>>{
        // 泛型数组
        protected T[] aux;
        // low, mid , high
        protected void merge(T[] nums, int l, int m, int h){
            int i = l, j = m + 1;
            for(int k = l; k <= h; k++){
                aux[k] = nums[k]; // 将数据复制到辅助数组
            }
            // i = low, j = mid+1
            // 从low 遍历到 high
            for(int k = l; k <= h; k++){
                if(i > m){
                    nums[k] = aux[j++];
                }   else if( j > h) {
                    nums[k] = aux[i++];
                }   else if( aux[i].compareTo(aux[j] <= 0)){
                    nums[k] = aux[i++]; // 先进行这一步,保证稳定性
                }   else{
                    nums[k] = aux[j++];
                }
            }
        }
   }

   2. 自顶向下归并排序: 将一个大数组分成两个小数组求解。每次都将问题对半分为两个子问题,这种对半分的算法复杂度一般为O(NlogN)
   public class Up2DownMergeSort<T extends Comparable<T>> extends MergeSort<T>{

        @Override
        public void sort(T[] nums){
            aux = (T[]) new Comparable[nums.length];
            sort(nums, 0, nums.length - 1);
        }

        private void sort(T[] nums, int l, int h){
            if(h <= l){
                return;
            }
            int mid = 1 + (h - l) /2;
            sort(nums, l, mid);
            sort(nums, mid+1, h);
            merge(nums, l, mid, h);
        }
   }
    3. 自底向上归并排序: 先归并哪些微型数组,然后成对归并得到的微型的数组
    public class Down2UpMergeSort<T extends Comparable<T>> extends MergeSort<T>{

        @Override
        public void sort(T[] nums){
            int N = nums.length;
            aux = (T[]) new Comparable[N];

            for(int sz = 1; sz < N; sz += sz){
                for(int lo = 0; lo < N -sz; lo += sz +sz){
                    merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz -1, N -1));
                }
            }

        }
    }

6. 快速排序: 通过一个切分元素将数组分为两个子数组,左子数组小于等于切分元素,右子数组大于等于切分元素,将这两个子数组排序也将整个数组排序
   public class QuickSort<T extends Comparable<T> extends Sort<T>>{

        @Override
        public void sort(T[] nums){
            // 先随机打乱
            shuffle(nums);
            sort(nums, 0, nums.length - 1);
        }

        private void sort(T[] nums, int l, int h){
            if( h <= l)
                return;
            // 前序遍历位置
            int j = partition(nums, l, h);
            sort(nums, l, j-1);
            sort(nums, j+1, h);
        }

        private void shuffle(T[] nums){
            List<Comparable> list = ArrayList.asList(nums);
            Collections.shuffle(list);
            list.toArray(nums);
        }
        // 切分: 取a[l]作为切分元素,从数组左端向右扫描知道找到第一个大于等于它的元素,再从左端找到第一个小于它的元素,交换这两个元素。
        private int partition(T[] nums, int l, int h){
            int i = l , j = h + 1;
            T v = nums[l];
            while(true){
                // 从左往右找第一个大于等于low的元素                
                while(less(nums[++i], v) && i != h);
                // 从右往左找第一个小于low的元素
                while(less(v, nums[--j]) && j != l);
                if( i >= j)
                    break;
                swap(nums, i, j);
            }
        }
   }
    2. 性能分析: 是原地排序,不需要辅助数组,但需要递归调用辅助栈。 最好的情况是每次都将数组对半分,递归调用次数最少。这种情况比较次数最少。O(NlogN)
                最坏情况是每次都是从最小元素切分。需要比较N2 /2 。为了防止初始有序,先打乱
    3. 算法改进: 因为再小数组中递归调用自己,对于小数组,插入排序比快速排序更好,可以切换到插入排序。
                 三数取中: 最好的情况是每次渠道中位数,但计算中位数代价高。折中方法是取3个元素,将居中元素作为切分元素
                 三向切分: 对于有大量重复元素的数组,可以切分为三部分,分别对应小于、等于和大于切分元素。可在线性时间完成重复元素的随机数组排序。

        public class ThreeWayQuickSort<T extends Comparable<T>> extends Quick<T>{
            @Override
            pretected void sort(T[] nums, int l, int h){
                if( h <= l)
                    return;
                int lt = l, i = l + 1; gt = h;
                T v = nums[l];
                while ( i <= gt ){
                    int cmp = nums[i].CompareTo(v); // low+1跟nums[low]比较
                    if(cmp < 0){
                        // 先交换,再++,
                        swap(nums, lt++, i++);
                    }   else if(cmp > 0){
                        // 交换low+1 和 high
                        swap(nums, i, gt--);
                    }   else{
                        i++;
                    }
                }
                sort(nums, l, lt - 1);
                sort(nums, gt+1, h);
            }
        }

    4. 基于切分的快速选择排序: 快速排序的partition()方法,会返回一个整数j,使得a[l...j-1] 小于等于a[j] , a[j+1...h] 大于等于a[j],此时a[j]就是数组第j大元素
        可以利用这个性质找到数组第[k]个元素。线性级别,假设能二分,则总比较次数是N + N/2 +N/4
        public T select(T[] nums, int k){
            int l = 0, h = nums.length - 1;
            while(h > l){
                int j = partition(nums, l, h);

                if(j == k){
                    return nums[k];
                }   else if(j > k){
                    h = j - 1;
                }   else {
                    l = j + 1;
                }
            }
            return nums[k];
        }

7. 堆排序: 堆中某个节点的值总是大于等于或者小于等于其子节点的值,并且堆是一颗完全二叉树。
            堆可以用数组来表示, 因为堆是完全二叉树,完全二叉树可以容易存储再数组中。位置K的节点父节点的位置为K/2, 而它的两个子节点是2k 和 2k+1。
            这里不适用数组索引为0的位置,是为了更清晰的描述节点的位置关系。
    1.public class Heap<T extends Comparable<T>> {
            private T[] heap;
            private int N = 0;

            public Head(int maxN){
                this.heap = (T[]) new Comparable[maxN + 1];
            }

            public boolean isEmpty(){
                return N == 0;
            }
            public int size(){
                return N;
            }
            private boolean less(int i, int j){
                return heap[i].compareTo(heap[j]) < 0;
            }
            private void swap(int i, int j){
                T t = heap[i];
                heap[i] = heap[j];
                heap[j] = t;
            }
        }

    2. 上浮和下沉。 一个节点比父节点大,那么交换两个节点。
        private void swim(int k){
            while(k > 1 && less(k/2, k)){ // 比较父节点和子节点的大小,父节点小,则交换位置
                swap(k / 2, k);
                k = k / 2;
            }
        }
        // 一个节点比子节点来的小,需要向下进行比较和交换。如果有两个子节点,应该和较大的节点交换。
        private void sink(int k){
            while(2* k <= N){   // 小于最大长度
                int j = 2 * k;  // 找到第一个子节点
                if(j < N && less(j, j + 1)) // 找到较大的子节点
                    j++;
                if(!less(k, j)) //如果父节点大于子节点 则break,小于则交换
                    break;
                swap(k, j);
                k = j;
            }
        }

    3. 插入元素。 将新元素放到数组末尾,然后上浮到合适的位置
        public void insert(Comparable v){
            heap[++N] = v;
            swim(N);
        }

    4. 删除最大元素。 从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。
        public T delMax(){
            T max = heap[1];  // 取顶端
            swap(1, N--);     // 交换顶端和最后一个元素
            heap[N + 1] = null; // 最后一个元素置为空
            sink(1);          // 下沉到合适位置
            return max;
        }
    5. 堆排序: 把最大元素和当前堆中数组最后一个元素交换位置,并且不删除它,那么可以得到一个从尾到头的递减序列,正向看就是递增序列。这就是堆排序
        构建堆: 无序数组建立堆的方法就是从左到右遍历数组进行上浮操作。 高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,
                那么下沉操作可以使得当前节点为根节点的堆有序。叶子节点不需要下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素

        public class HeapSort<T extends Comparable<T>> extends Sort<T>{
            // 数组0的位置不能有元素
            @Override
            public void sort(T[] nums){
                // 建堆
                int N = nums.length - 1;
                for(int k = N / 2; k >= 1; k--)
                    sink(nums, k, N);
                // 堆排序
                while( N > 1){
                    swap(nums, 1, N--); //交换位置
                    sink(nums, 1, N);   // 下沉
                }
            }

            private void sink(T[] nums, int k , int N){
                while(2 * k <= N){
                    int j = 2 * k ; // 找到子节点
                    if( j <= N && less(nums, j, j+1))
                        j++;        // 找到较大的子节点
                    if(!less(nums, k, j))
                        break;      // 如果父节点> 子节点,结束
                    swap(nums, k, j);   // 否则交换节点
                    k = j;
                }
            }

            private boolean less(T[] nums, int k, int j){
                return nums[i].CompareTo(nums[j]) < 0;
            }
        }

        一个堆的高度是 logN, 因此插入和删除元素的复杂度都是logN. 堆排序需要堆N个节点进行操作,复杂度为 N log N;
        是一种原地排序,没有额外空间。很少使用因为无法利用局部性原理缓存

8. 总结:
   1. 稳定性: 冒泡排序、 插入排序、 归并排序
   2. 空间复杂度为1: 选择排序、 冒泡排序、 插入排序、 希尔排序、归并排序
   3. 时间复杂度为N-log N: 快速排序, 归并排序、 堆排序
   4. 快速排序是最快的通用排序算法,内循环指令少,还能利用缓存,总是顺序访问数据。
   5. 三向切分快速排序(大量重复主键),实际可能出现的某些分布可能达到线性级别,而其他排序算法仍需要线性对数时间。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值