十大排序基本思路以及实现加优化(java实现)思路清晰

排序

D:\my_java\java_doc\笔记\数据结构\img\image-20201203154015918.png

父类设计

为了测试方便,设计了Sort抽象父类,父类中有需要排序的数组、交换次数等成员变量。后面的各个排序都继承这个父类,重写sort方法,使用的适合用上转型实例化,比如:Sort quickSort = new QuickSort();然后调用父类sort方法自动调用quickSort子类方法,并计算比较次数 、耗费时间等,打印对象即可获取这些信息

package sort;

import java.util.Arrays;

/**
 * @author FatTiger
 * @date 2020-12-04 14:54
 **/
public abstract class Sort {
    protected Integer array[];
    private long cmpCount;
    private long swapCount;
    public long time;


    public void sort(Integer array[]) {
        if (array == null || array.length < 2) return;

        this.array = array;

        long begin = System.currentTimeMillis();
        sort();
        time = System.currentTimeMillis() - begin;
    }

    protected abstract void sort();

    protected int cmp(int i1, int i2) {
        cmpCount++;
        return array[i1] - array[i2];
    }

    protected int cmp1(int value1, int value2) {
        cmpCount++;
        return value1 - value2;
    }

    protected void swap(int i1, int i2) {
        swapCount++;
        int tmp = array[i1];
        array[i1] = array[i2];
        array[i2] = tmp;

    }

    @Override
    public String toString() {
        return this.getClass().getName() + "\t{\t" +
                "\t比较次数=" + cmpCount +
                "\t交换次数=" + swapCount +
                "\t耗费时间 =" + time +
                '}';
    }

    public String toStringArr() {
        return Arrays.toString(array);
    }
}

冒泡排序

执行流程

  • 从头开始比较每一对相邻元素,如果第一个比第二个大,就交换他们的位置
    • 执行完一轮之后,最后的元素就是最大的元素
  public static void bubbleSort(int array[]) {
        for (int i = 0; i < array.length; i++) {
            for (int begin = 1; begin < array.length - i; begin++) {

                if (array[begin] < array[begin - 1]) {
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                }

            }
        }
        
    }

优化1

如果序列已经完全有序,可以提前终止冒泡排序

每次循环加入标记,如果有一次不符合前面一个比后面一个要小于(大于)的情况,就置为false,在每次循环走完之后判断是否为true,即可做到有序时提前终止排序

public static void bubbleSort(int array[]) {
        for (int i = 0; i < array.length; i++) {
            boolean sorted = true;
            for (int begin = 1; begin < array.length - i; begin++) {
                if (array[begin] < array[begin - 1]) {
                    sorted = false;
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                }

            }
            if (sorted) {
                break;
            }
        }

    }

优化2

如果序列尾部已经局部有序,可以记录最后一次交换的位置,减少比较次数

 public static void bubbleSort(Integer array[]) {

        for (int end = array.length - 1; end > 0 ; end--) {
            int sortedIndex = 1;
            for (int begin = 1; begin <= end; begin++) {
                if (array[begin] < array[begin - 1]) {
                    sortedIndex = begin;
                    int tmp = array[begin];
                    array[begin] = array[begin - 1];
                    array[begin - 1] = tmp;
                }

            }

            end = sortedIndex;
        }

    }

稳定性

  • 如果相等的两个元素,在排序前后的相对位置保持不变,那么这是稳定的排序算法
  • 冒泡是稳定的

原地算法(in-place Algorithm)

不依赖额外的资源或者依赖少数的额外资源,近依靠输出来覆盖输入

代码

public class BubbleSort extends Sort {


    @Override
    protected void sort() {
        for (int end = array.length - 1; end > 0 ; end--) {
            int sortedIndex = 1;
            for (int begin = 1; begin <= end; begin++) {
                if (cmp(begin, begin-1) < 0) {
                    sortedIndex = begin;

                    swap(begin, begin-1);
                }

            }

            end = sortedIndex;
        }
    }
}

选择排序

执行流程

  • 从序列中找出最大的那个元素,然后与最末尾的元素交换位置
    • 执行完一轮后,最末尾的那个元素就是最大的元素
  • 忽略曾经找到的最大元素,重复执行步骤1
public class SelectionSort extends Sort {
    @Override
    protected void sort() {
        for (int end = array.length - 1; end > 0 ; end--) {
            int maxIndex = 0;
            for (int begin = 1; begin <= end; begin++) {
                if (cmp(begin, maxIndex) >= 0) {
                    maxIndex = begin;
                }
            }
            swap(maxIndex, end);
        }

    }
}

选择排序的交换次数远远小于冒泡排序,平均性能要优于冒泡

优化

使用堆

堆排序(heap sort)

  • 堆排序可以认为是对选择排序的一种优化

执行流程

  • 对序列进行原地建堆(heapify)
  • 重复执行以下操作,直到堆的元素数量为1
    • 交换堆顶元素与尾元素
    • 堆的元素数量-1
    • 对0位置进行一次siftDown操作

代码

public class HeapSort extends Sort {
	private int heapSize;

	@Override
	protected void sort() {
		// 原地建堆
		heapSize = array.length;
		for (int i = (heapSize >> 1) - 1; i >= 0; i--) {
			siftDown(i);
		}
		
		while (heapSize > 1) {
			// 交换堆顶元素和尾部元素
			swap(0, --heapSize);

			// 对0位置进行siftDown(恢复堆的性质)
			siftDown(0);
		}
	}
	
	private void siftDown(int index) {
		Integer element = array[index];
		
		int half = heapSize >> 1;
		while (index < half) { // index必须是非叶子节点
			// 默认是左边跟父节点比
			int childIndex = (index << 1) + 1;
			Integer child = array[childIndex];
			
			int rightIndex = childIndex + 1;
			// 右子节点比左子节点大
			if (rightIndex < heapSize && 
					cmp1(array[rightIndex], child) > 0) {
				child = array[childIndex = rightIndex];
			}
			
			// 大于等于子节点
			if (cmp1(element, child) >= 0) break;
			
			array[index] = child;
			index = childIndex;
		}
		array[index] = element;
	}
}

插入排序(insertion Sort)

插入排序非常类似于扑克牌的排序

执行流程

  • 在执行过程中,插入排序会将序列分为2部分
    • 头部是已经排好序的,尾部是待排序的
  • 从头开始扫描每一个元素
    • 每当扫描到一个元素,就将它插入到头部合适的位置,使得头部数据依然保持有序
 for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            while (cur > 0 && cmp(cur, cur-1) > 0) {
                swap(cur, cur-1);
                cur--;
            }
        }

逆序对(invertion)

  • 什么是逆序对
    • 数组<2,3,8,6,1>的逆序对为:<2,1> ❤️,1> <8,1> <8,6> <6,1>,共五个逆序对
  • 插入排序的时间复杂度与逆序对的数量成正比关系
    • 逆序对的数量越多,插入排序的时间复杂度越高
  • 最坏、平均复杂度为O(n2)
  • 最好的复杂度是O(n)
  • 当逆序对的数量极少时,插入排序的效率特别高
    • 甚至比O(nlogn)级别的快速排序还要快

优化

思路是将【交换】转为【挪动】

  • 先将待插入的元素备份
  • 头部有序数据中比待插入元素大的,都朝尾部方向挪动一个位置
  • 将待插入元素放到最终的合适位置
 for (int begin = 1; begin < array.length; begin++) {
            int cur = begin;
            int v = array[cur];
            while (cur > 0 && cmp(cur, cur-1) > 0) {
                array[cur] = array[cur - 1];
                cur--;
            }
            array[cur] = v;
        }

在这里插入图片描述

二分搜索

如何确定一个元素在数组中的位置?(假设数组里全是整数)

  • 如果是无序数组,从第0个位置开始遍历搜索,平均时间复杂度:O(n)
  • 如果是有序数组,可以使用二分搜索,最坏时间复杂度:O(logn)
思路
  • 假设在[begin,end)范围内搜索某个元素v,mid == (begin + end) / 2
  • 如果v < m ,去[begin,mid)范围内二分搜索
  • 如果v > m,去[mid+1,end)范围内二分搜索
  • 如果v == m,直接返回mid
思考

如果存在多个重复的值返回哪一个?

无规律

插入排序-二分搜索优化

  • 在元素v的插入过程中,可以先二分搜索除合适的插入位置,然后再将元素v插入
  • 要求二分搜索返回的插入位置:第一个大于v的元素位置
思路
  • 假设在[begin,end)范围内搜索某个元素v,mid == (begin + end) / 2
  • 如果v < m ,去[begin,mid)范围内二分搜索
  • 如果v >= m,去[mid+1, end) 范围内二分搜索

代码

 public class InsertionSort extends Sort {
    @Override
    protected void sort() {
        for (int begin = 1; begin < array.length; begin++) {
            int v = array[begin];
            int insertIndex = search(begin);

            for (int i = begin; i > insertIndex; i--) {
                array[i] = array[i - 1];
            }
            array[insertIndex] = v;
        }
    }

    public int search(int index) {
        int v = array[index];

        int begin = 0;
        int end = index;

        while (begin < end) {
            int mid = (begin + end) >> 1;
            if (cmp1(v, array[mid]) < 0) {
                end = mid;
            } else {
                begin = mid + 1;
            }
        }

        return begin;
    }
}

归并排序(Merge Sort)

执行流程

  • 不断地将当前序列平均分割成两个子序列
    • 直到不能再分割(序列中只剩一个元素)
  • 不断地将2个子序列合并成一个有序序列
    • 直到最终只剩下一个有序序列

divide实现

/**
     * 对[begin,end)范围的数据进行归并排序
     * @param begin
     * @param end
     * @return void
     * @author FatTiger
     * @date 2020/12/6 13:27
     */
    private void sort(int begin, int end) {
        if (end - begin < 2) return;

        int mid =  (begin + end) >> 1;

        sort(begin, mid);
        sort(mid, end);
        merge(begin,mid,end);
    }

merge实现

在这里插入图片描述
在这里插入图片描述

 /**
     * 将[begin,mid) 和 [mid,end)合并成一个有序的序列
     * @param begin
     * @param mid
     * @param end
     * @return void
     * @author FatTiger
     * @date 2020/12/6 13:39
     */
    private void merge(int begin, int mid, int end) {
        //左边数组(基于leftArray)
        int li = 0,le = mid - begin;
        //右边数组(基于array)
        int ri = mid, re = end;
        //array的索引
        int ai = begin;

        //备份左边数组
        for (int i = li; i < le; i++) {
            leftArray[i] = array[begin + i];
        }

        //如果左边还没有结束
        while (li < le) {

            //如果右边小拷贝右边元素到array
            if (ri < re && cmp1(array[ri], leftArray[li]) < 0) {
                array[ai++] = array[ri++];
            } else {
                //如果左边小或者相等,就将左边元素拷贝到Array
                array[ai++] = leftArray[li++];

            }
            //cmp1位置改成<=就会不稳定
        }
    }

复杂度分析

在这里插入图片描述

代码

public class MergerSort extends Sort {
    private int[] leftArray;
    @Override
    protected void sort() {
        leftArray = new int[array.length >> 1];
        sort(0, array.length);
    }

    /**
     * 对[begin,end)范围的数据进行归并排序
     * @param begin
     * @param end
     * @return void
     * @author FatTiger
     * @date 2020/12/6 13:27
     */
    private void sort(int begin, int end) {
        if (end - begin < 2) return;

        int mid =  (begin + end) >> 1;

        sort(begin, mid);
        sort(mid, end);
        merge(begin,mid,end);
    }

    /**
     * 将[begin,mid) 和 [mid,end)合并成一个有序的序列
     * @param begin
     * @param mid
     * @param end
     * @return void
     * @author FatTiger
     * @date 2020/12/6 13:39
     */
    private void merge(int begin, int mid, int end) {
        //左边数组(基于leftArray)
        int li = 0,le = mid - begin;
        //右边数组(基于array)
        int ri = mid, re = end;
        //array的索引
        int ai = begin;

        //备份左边数组
        for (int i = li; i < le; i++) {
            leftArray[i] = array[begin + i];
        }

        //如果左边还没有结束
        while (li < le) {

            //如果右边小拷贝右边元素到array
            if (ri < re && cmp1(array[ri], leftArray[li]) < 0) {
                array[ai++] = array[ri++];
            } else {
                //如果左边小或者相等,就将左边元素拷贝到Array
                array[ai++] = leftArray[li++];

            }
            //cmp1位置改成<=就会不稳定
        }
    }
}

快速排序⭐(quickSort)

执行流程

  • 从序列中选择一个轴点元素(pivot)
    • 假设每次选择0位置的元素为轴点元素
  • 利用pivot将序列分割成2个子序列
    • 将小于pivot的元素放在pivot前面(左侧)
    • 将大于pivot的元素放在pivot后面(右侧)
    • 等于pivot的元素放哪边都可以
  • 对子序列进行上面的操作
    • 直到不能再分割(子序列中只剩下1个元素)

在这里插入图片描述

本质

逐渐将每个元素都变成轴点元素

轴点构造

在这里插入图片描述

最坏情况

左右两边元素极度不均匀的情况

解决办法

随机选择轴点元素

代码

   public class QuickSort extends Sort {

    @Override
    protected void sort() {
        sort(0, array.length);
    }

    /**
     * 对[begin,end)进行快速排序
     * @param begin
     * @param end
     * @return void
     * @author FatTiger
     * @date 2020/12/7 11:05
     */
    private void sort(int begin, int end) {
        if (end - begin < 2) return;

        //确定轴点位置
        int mid = pivotIndex(begin,end);

        //对子序列进行快速排序

        sort(begin, mid);
        sort(mid + 1, end);

    }

    /**
     * 确定轴点元素位置
     * @param begin
     * @param end
     * @return int 轴点元素的最终位置
     * @author FatTiger
     * @date 2020/12/7 11:07
     */
    private int pivotIndex(int begin, int end) {
        //随机选择轴点元素与头位置交换
        swap(begin, begin + (int) (Math.random() * (end - begin)));

        //备份begin位置的元素
        int pivot = array[begin];
        //end要指向到最后一个元素
        end--;

        while (begin < end) {

            while (begin < end) {
                if (cmp1(pivot,array[end]) < 0) {//右边元素大于轴点元素
                    end--;
                } else {//右边元素小于等于轴点元素
                    array[begin++] = array[end];
                    break;
                }
            }

            while (begin < end) {
                if (cmp1(pivot,array[begin]) > 0) {//左边元素小于轴点元素
                    begin++;
                } else { //左边元素大于等于轴点元素
                    array[end--] = array[begin];
                    break;
                }
            }

        }

        //将轴点元素放入最终的位置
        array[begin] = pivot;

        return begin;
    }
}

希尔排序(shell sort)

  • 希尔排序把序列看作是一个矩阵,分成m列,逐列进行排序
    • m从某个整数逐渐减为1
    • 当m为1时,整个序列将完全有序
  • 因此,希尔排序也被称为递减增量排序
  • 矩阵的列数取决于步长序列
    • 比如,如果步长序列为{1,5,19,41,109,…},就代表依次分成109列、41列、19列、5列、1列进行排序
    • 不同的步长序列,执行效率也不同

代码

   public class ShellSort extends Sort {

    @Override
    protected void sort() {
        List<Integer> stepSequence = shellStepSequence();

        for (Integer step : stepSequence) {
            sort(step);
        }
    }

    private void sort(Integer step) {
        for (int col = 0; col < step; col++) {
            //对col列进行排序
            for (int begin = col + step; begin < array.length; begin += step) {
                int cur = begin;
                while (cur > col && cmp(cur,cur-step) < 0) {
                    swap(cur,cur-step);
                    cur -= step;
                }
            }
        }
    }
	//采用shell的步长序列{16,8,4,2,1}
    private List<Integer> shellStepSequence() {
        ArrayList<Integer> stepSequence = new ArrayList<>();
        int step = array.length;
        while ((step = step >> 1) > 0) {
            stepSequence.add(step);
        }
        return stepSequence;
    }


}

计数排序(counting sort)

  • 之前学习的冒泡、选择、插入、归并、快速、希尔、堆排序,都是基于比较的排序
    • 平均时间复杂度目前最低是O(nlogn)
  • 计数排序、桶排序、基数排序,都不是基于比较的排序
    • 他们是典型的用空间换时间,在某些时候,平均时间复杂度可以比O(nlogn)更低
  • 核心思想
    • 统计每个整数在序列中出现的次数,进而推导除每个整数在有序序列中的索引

最简单的实现

 //找出最大值
        int max = array[0];
        for (int i = 0; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
        }

        int counts[] = new int[max + 1];

        //统计每个整数出现的次数

        for (int i = 0; i < array.length; i++) {
            counts[array[i]]++;
        }
        int j = 0;
        //根据整数出现次数,对整数进行排序
        for (int i = 0; i < counts.length; i++) {
			//放入原数组
            while (counts[i] > 0) {
                array[j++] = i;
                counts[i]--;
            }
        }
存在的问题
  • 无法对负整数进行排序
  • 极其浪费内存空间
  • 是个不稳定的排序

改进思路

在这里插入图片描述

改进实现

 public class CountSort extends Sort {
    @Override
    protected void sort() {
        //找出最值
        int max = array[0];
        int min = array[0];
        for (int i = 0; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if (array[i] < min) {
                min = array[i];
            }
        }

        //存储次数
        int counts[] = new int[max - min + 1];

        //统计每个整数出现的次数
        for (int i = 0; i < array.length; i++) {

            counts[array[i] - min]++;
        }
        //累加次数
        for (int i = 1; i < counts.length; i++) {
            counts[i] += counts[i - 1];
        }

        //从后往前遍历元素,将它放到有序数组中的合适位置
        Integer newArray[] = new Integer[array.length];

        for (int i = array.length - 1; i >= 0; i--) {
            newArray[ --counts[array[i]-min]] = array[i];
        }

        for (int i = 0; i < array.length; i++) {
            array[i] = newArray[i];
        }
    }
}

基数排序(Radix Sort)

  • 基数排序非常适合用于整数排序(尤其是非负整数)
  • 执行流程:依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)

排序测试(参考)

public class Main implements Serializable {



     public static void main(String[] args) {
        Integer[] array = Integers.random(30000, 1, 50000);


        testSorts(array,
                    new InsertionSort(),
                    new MergerSort(),
                    new BubbleSort(),
                    new SelectionSort(),
                    new HeapSort(),
                    new QuickSort(),
                    new ShellSort(),
                    new CountSort()
                );

    }

    public static void  testSorts(Integer array[], Sort... sorts) {

        for (Sort sort : sorts) {
            Integer[] array1 = Integers.copy(array);

            sort.sort(array1);
            Asserts.test(Integers.isAscOrder(array1));
        }
        Arrays.sort(sorts, new Comparator<Sort>() {
            @Override
            public int compare(Sort o1, Sort o2) {
                return (int) (o1.time - o2.time);
            }
        });

        for (Sort sort : sorts) {
            System.out.println(sort);
        }
    }



}

结果

在这里插入图片描述

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值