JAVA排序算法

By timebusker on February 25, 2016
package com.timebusker.algorithm.sort;

import com.timebusker.algorithm.AbstractAlgorithm;
import org.junit.Test;

import java.util.Arrays;

/**
 * @Description: AlgorithmSort:排序算法
 * <p>
 * 参考博客地址:
 * https://www.cnblogs.com/guoyaohua/p/8600214.html
 * @Author: Administrator
 * @Date: 2020/1/17 14:10
 **/
public class SortAlgorithm extends AbstractAlgorithm {

    /**
     * 选择排序:每一轮排序,选择数组中数字最小的那一个放到指定的位置上。
     * 时间复杂度o(n^2),无论数组顺序如何都要选择一个最小的值,
	 * 因为数组的是否有序,不影响时间复杂度
     * 空间复杂度o(1)
     * 不稳定排序
     */
    @Test
    public void chooseSort() {
        int length = array.length;
        for (int i = 0; i < length; i++) {
            // 申请额外的空间o(1)
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (array[min] > array[j]) {
                    min = j;
                }
            }
            //将最小的下标代表的数与i位置的进行交换
            int tmp = array[i];
            array[i] = array[min];
            array[min] = tmp;
        }
        println(array);
    }

    /**
     * 直接插入排序:每一轮排序,都是在i坐标之前,包括i坐标的序列是有序的,但是并不是最终的排序位置。
     * 时间复杂度o(n^2),对于第二重循环,只会在非有序的环境下才会执行每个元素后移,
	 * 因此针对有序的数组,时间复杂度最好的情况是o(N)。
     * 空间复杂度o(1)
     * 稳定排序
     */
    @Test
    public void insertDirectlySort() {
        int length = array.length;
        for (int i = 1; i < length; i++) {
            for (int j = i; j > 0; j--) {
                // 这一步导致该算法是稳定排序
                if (array[j] < array[j - 1]) {
                    int tmp = array[j - 1];
                    array[j - 1] = array[j];
                    array[j] = tmp;
                }
            }
        }
        println(array);
    }

    /**
     * 折半插入排序:针对直接排序而言,每一个要插入的元素都是插入在有序的数组中,
	 * 因此,只需要查找到插入的位置即可,查找的方式利用二分查找
     * 时间复杂度和直接插入是一样的,只是快在了查找的过程中,还是o(N^2),最好的环境下是o(N)
     * 空间复杂度还是o(1)
     */
    @Test
    public void insertBinarySort() {
        int length = array.length;
        for (int i = 1; i < length; i++) {
            int tmp = array[i];
            int low = 0;
            int high = i - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                if (tmp < array[mid]) {
                    high = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
            for (int j = i; j >= low + 1; j--) {
                array[j] = array[j - 1];
            }
            array[low] = tmp;
        }
        println(array);
    }

    /**
     * 冒泡排序,每i轮排序,就是不断交换两个元素,直到将最大的元素放到(n-i)的位置上
     * 这种实现是按照算法定义的,但是效率是最低的
     * 时间复杂度o(n^2)
     * 空间复杂度o(1)
     * 稳定排序
     */
    @Test
    public void bubbleSort1() {
        int length = array.length;
        for (int i = 1; i < length; i++) {
            for (int j = 0; j < length - i; j++) {
                //这一步导致该算法是稳定排序
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                }
            }
        }
        println(array);
    }

    /**
     * 冒泡排序:高效率实现,因为只需要用一个flag变量来记录本次的排序,是否修改;如果没有修改,说明已经有序
     */
    @Test
    public void bubbleSort2() {
        int length = array.length;
        boolean flag = true;
        while (flag) {
            flag = false;
            for (int j = 0; j < length - 1; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                    flag = true;
                }
            }
            length--;
        }
        println(array);
    }

    /**
     * 归并排序,将数组一分为二,对于每一子数组继续进行上述步骤,直到子数组只有1个元素,那么自然是有序的。
     * 然后不断合并两个数组,直到合并到整个数组。
     * 时间复杂度o(NlgN)
     * 空间复杂度o(N)
     * <p>
     * 归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。
	 * 代价是需要额外的内存空间。
     */
    @Test
    public void mergeSort() {
        int length = array.length;
        int low = 0;
        int high = length - 1;
        realSort(array, low, high);
        println(array);
    }

    /**
     * 归并排序真正的sort函数
     *
     * @param array 待排序的数组
     * @param low   最低位
     * @param high  最高位
     */
    private void realSort(int[] array, int low, int high) {
        int mid = (low + high) / 2;
        if (low < high) {
            realSort(array, low, mid);
            realSort(array, mid + 1, high);
            realMerge(array, low, mid, high);
        }
    }

    private void realMerge(int[] array, int low, int mid, int high) {
        int[] tmp = new int[high - low + 1];
        int leftPoint = low;
        int rightPoint = mid + 1;
        int index = 0;
        while (leftPoint <= mid && rightPoint <= high) {
            if (array[leftPoint] < array[rightPoint]) {
                tmp[index++] = array[leftPoint++];
            } else {
                tmp[index++] = array[rightPoint++];
            }
        }
        while (leftPoint <= mid) {
            tmp[index++] = array[leftPoint++];
        }
        while (rightPoint <= high) {
            tmp[index++] = array[rightPoint++];
        }
        System.arraycopy(tmp, 0, array, low, tmp.length);
    }

    /**
     * 快速排序,选定一个切分元素,每一轮排序后,都保证切分元素之前的元素都小于切分元素,切分元素之后的元素都大于切分元素
     * <p>
     * 通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,
	 * 则可分别对这两部分记录继续进行排序,以达到整个序列有序。
     * <p>
     * 从数列中挑出一个元素,称为 “基准”(pivot);
     * 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。
	 * 在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
     * 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
     * <p>
     * 时间复杂度o(NlgN)
     * 空间复杂度o(lgN)
     * 不稳定排序
     */
    @Test
    public void quickSort() {
        int low = 0;
        int high = array.length - 1;
        sort(array, low, high);
        println(array);
    }

    /**
     * 快速排序的递归实现
     *
     * @param array
     * @param low
     * @param high
     */
    public void sort(int[] array, int low, int high) {
        if (low >= high) {
            return;
        }
        int j = partition(array, low, high);
        sort(array, low, j - 1);
        sort(array, j + 1, high);
    }

    /**
     * 快速排序的辅助方法,来对排序的数组,进行切分,
     *
     * @param array
     * @param low
     * @param high
     * @return
     */
    public int partition(int[] array, int low, int high) {
        int i = low;
        int j = high;
        int x = array[i];
        while (i < j) {
            // 从右向左找到array[j]小于x的元素
            while (i < j && array[j] >= x) {
                j--;
            }
            if (i < j) {
                array[i] = array[j];
                i++;
            }
            // 从左向右找到array[i]大于x的元素
            while (i < j && array[i] <= x) {
                i++;
            }
            if (i < j) {
                array[j] = array[i];
                j--;
            }
        }
        array[i] = x;
        return i;
    }


    /**
     * 堆排序,建立一个小顶堆,小顶堆满足父节点比两个子节点的值要小
     * <p>
     * 堆的性质满足:
     * 1. 只能在堆顶删除元素
     * 2. 只能在堆的最后一位存元素。
     * 3. 堆的存储利用数组,满足i节点是父节点,则子节点是2 * i+ 1,2 * i + 2
     * 4. 堆的两种建方法,第一种是从上到下,@see sink(),第二种是从下到上 @see swim
     * 5. 堆排序是指在弄好的堆中,输出第一个元素,然后将最后一个元素与第一个元素互换,
	 * 	  换后调用sink,找到自己的位置后,在重复这个步骤,就输出一个有序的堆
     * 6. 如果要生序就需要大顶堆,要降序就需要小顶堆。
     * <p>
     * 时间复杂度:o(NlgN)
     * 空间复杂度: o(1)
     * 这是小顶堆的排序,所以array数组最后是降序的
     * 不稳定,不稳定的原因在建堆的时候,就可能把相同元素的位置换了,比如两个相同元素在不同的子树上
     */
    @Test
    public void heapSort() {
        int length = array.length;
        // 只能从前一半开始sink
        for (int i = length / 2; i >= 0; i--) {
            sink(array, i, length);
        }
        while (length > 0) {
            int temp = array[0];
            array[0] = array[length - 1];
            array[length - 1] = temp;
            length--;
            sink(array, 0, length);
        }
    }

    /**
     * 将i放入对堆中,i的父节点是(i - 1)/ 2,父节点的值是小于他的两个子节点的
     * i节点放入后,要向上移动,如果父节点比i节点的值大,则i节点要继续上移。
     *
     * @param array
     * @param i
     */
    private void swim(int array[], int i) {
        while (i > 0) {
            int father = (i - 1) / 2;
            if (array[father] > array[i]) {
                int temp = array[father];
                array[father] = array[i];
                array[i] = temp;
            }
            i = father;
        }
    }


    /**
     * 从i节点由上到下开始调整,i节点的子节点为2*i + 1, 2 * i + 2
     * i节点要向下移动,直到满足i节点小于两个子节点
     *
     * @param array array[] 数组
     * @param i     i节点
     */
    public void sink(int[] array, int i, int n) {
        int son = 2 * i + 1;
        while (son <= n - 1) {
            if (son < n - 1 && array[son] > array[son + 1]) son++;
            if (array[i] > array[son]) {
                int temp = array[i];
                array[i] = array[son];
                array[son] = temp;
                i = son;
                son = 2 * i + 1;
            } else {
                break;
            }
        }
    }
}