常用算法的复习和总结

常用算法的复习和总结

排序分类

排序是《数据结构》一书中的重要内容,在软件开发领域里也有非常重要的作用。

排序算法按照空间划分分为内部排序和外部排序,内部排序顾名思义就是直接在原数组上进行的排序;外部排序意思就是需要借助额外的空间在辅助进行排序。

按照排序方式来分,又分为插入排序、选择排序、交换排序、归并排序、基数排序,其中插入排序有直接插入排序和希尔排序,选择排序有简单选择排序和堆排序,交换排序有冒泡排序和快速排序

除此之外,还有计数排序、桶排序等等,基本上是在已有算法基础上的改进和优化,下边就复习一下这些算法

算法介绍及示例

为了能更好的复习和总结这些排序算法以及展示示例,先做一些准备工作

public class SortDemo {

    private int[] arr = new int[10];

    @BeforeEach
    public void before() {
        Random random = new Random();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(100);
        }
        System.out.println("原数组:" + Arrays.toString(arr));
    }

    @AfterEach
    public void after() {
        System.out.println("排序后:" + Arrays.toString(arr));
    }

    /**
     * 数组中两个位置的元素交换
     */
    private void swap(int[] arr, int i, int j) {
        if (i == j) {
            return;
        }
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

冒泡排序

冒泡排序的原理是,从左往右遍历数组,每一个数与其之前的那个数比较,使相对打的那个数排在右边,相对小的那个数排在左边,如此遍历一次,可将数组中最大的数排在最右边,然后再遍历剩下的数。以此类推,最终将数组变成有序数组

算法复杂度:O(n²)

空间复杂度:O(1)

/**
 * 冒泡排序(从小到大)
 * 原理:每一个数与其后边的数比较,小数在前,大数在后,通过每次递进交换位置实现排序
 * 1.对于一个长度为n的数组,从第0个元素开始,每一个与其后边的一个比较大小,
 * 若N[i] > N[i + 1],则两个元素交换,swap(arr, i, i + 1),此时i∈[0, n - 1]
 * 2.领i的集合范围中,右边缘的值减1,即i∈[0, n - 2],执行1中操作,知道i只能等于0为止
 * 算法复杂度:O(n²)
 * 空间复杂度:O(1)
 */
@Test
public void bubbleSort() {
    if (arr.length <= 1) {
        return;
    }
    for (int i = arr.length - 1; i >= 0; i--) {
        for (int j = 0; j < i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            }
        }
        System.out.println(Arrays.toString(arr) + ", i=" + String.valueOf(i));
    }
}

执行效果如下:

原数组:[7, 8, 3, 5, 6, 4, 4, 4, 4, 3]

[7, 3, 5, 6, 4, 4, 4, 4, 3, 8], i=9

[3, 5, 6, 4, 4, 4, 4, 3, 7, 8], i=8

[3, 5, 4, 4, 4, 4, 3, 6, 7, 8], i=7

[3, 4, 4, 4, 4, 3, 5, 6, 7, 8], i=6

[3, 4, 4, 4, 3, 4, 5, 6, 7, 8], i=5

[3, 4, 4, 3, 4, 4, 5, 6, 7, 8], i=4

[3, 4, 3, 4, 4, 4, 5, 6, 7, 8], i=3

[3, 3, 4, 4, 4, 4, 5, 6, 7, 8], i=2

[3, 3, 4, 4, 4, 4, 5, 6, 7, 8], i=1

[3, 3, 4, 4, 4, 4, 5, 6, 7, 8], i=0

排序后:[3, 3, 4, 4, 4, 4, 5, 6, 7, 8]

选择排序

选择排序的原理是遍历一次数组,找出数组中的最大数(或最小数)并将这个数与数组的第一个数交换,遍历数组中剩下的数,继续上述操作,最终完成数组的排序

算法复杂度:O(n²)

空间复杂度:O(1)

/**
 * 选择排序(从小到大)
 * 原理:对于一个长度为n的数组,从数组选择出最小的数,将其位置与N[0]交换,然后从N[1]开
 * 始遍历,以此类推,直到从N[n-1]开始遍历为止
 * 算法复杂度:O(n²)
 * 空间复杂度:O(1)
 */
@Test
public void selectionSort() {
    if (arr.length <= 1) {
        return;
    }
    int minIndex = -1;
    for (int i = 0; i < arr.length; i++) {
        minIndex = i;
        for (int j = i; j < arr.length; j++) {
            if (arr[minIndex] > arr[j]) {
                minIndex = j;
            }
        }
        swap(arr, i, minIndex);
        System.out.println(Arrays.toString(arr) + ", i=" + String.valueOf(i));
    }
}

执行效果如下:

原数组:[7, 2, 1, 7, 0, 9, 1, 5, 3, 3]

[0, 2, 1, 7, 7, 9, 1, 5, 3, 3], i=0

[0, 1, 2, 7, 7, 9, 1, 5, 3, 3], i=1

[0, 1, 1, 7, 7, 9, 2, 5, 3, 3], i=2

[0, 1, 1, 2, 7, 9, 7, 5, 3, 3], i=3

[0, 1, 1, 2, 3, 9, 7, 5, 7, 3], i=4

[0, 1, 1, 2, 3, 3, 7, 5, 7, 9], i=5

[0, 1, 1, 2, 3, 3, 5, 7, 7, 9], i=6

[0, 1, 1, 2, 3, 3, 5, 7, 7, 9], i=7

[0, 1, 1, 2, 3, 3, 5, 7, 7, 9], i=8

[0, 1, 1, 2, 3, 3, 5, 7, 7, 9], i=9

排序后:[0, 1, 1, 2, 3, 3, 5, 7, 7, 9]

插入排序

插入排序的原理是遍历数组中的元素,每一个元素和它左边的元素比较,并保持该元素左边的元素集合始终是从小到大或者从大到小的状态,这样,一次遍历完成后,可使数组变成有序数组

插入排序的关键在于遍历过程中,N[i]要与之前的元素比较和交换,直至某一次交换后,N[i]及其左边的数保持有序即可停止交换

算法复杂度:插入排序是一种不稳定的排序,复杂度受数组原顺序影响,范围从O(n)到O(n²),最好情况是数组已经有序,则只遍历一次不交换,即O(n),最坏情况比如要将数组从小到大排序,而数组原顺序是从大到小,则每次遍历都会进行交换,交换次数为从1累加到(n-1),即0.5*n²,平均情况下为O(n1.3)

空间复杂度:O(1)

/**
 * 插入排序(从小到大)
 * 原理:
 * 对于一个长度为n(n>1)的数组,从N[1]开始进行遍历,若存在一个元素N[i](i≥1),
 * 使得N[i] < N[i-1],则两个元素交换位置,交换后,从N[i-1]开始向前遍历,
 * 直到存在一个值k(k≥1),使得N[k] > N[k-1],即保证了N[i-1]及其之前的元素
 * 都是从小到大排列的,进行下次循环
 * 算法复杂度:不稳定,从O(n)到O(n²)
 * 空间复杂度:O(1)
 */
@Test
public void insertionSort() {
    if (arr.length <= 1) {
        return;
    }
    for (int i = 1; i < arr.length; i++) {
        for (int j = i; j >= 1; j--) {
            if (arr[j - 1] > arr[j]) {
                swap(arr, j - 1, j);
            } else {// 这个节点之前的元素已经是排序好的,即都是从小到大的
                break;
            }
        }
        System.out.println(Arrays.toString(arr) + ", i=" + String.valueOf(i));
    }
}

执行结果如下:

原数组:[5, 9, 9, 8, 0, 7, 5, 3, 7, 6]

[5, 9, 9, 8, 0, 7, 5, 3, 7, 6], i=1

[5, 9, 9, 8, 0, 7, 5, 3, 7, 6], i=2

[5, 8, 9, 9, 0, 7, 5, 3, 7, 6], i=3

[0, 5, 8, 9, 9, 7, 5, 3, 7, 6], i=4

[0, 5, 7, 8, 9, 9, 5, 3, 7, 6], i=5

[0, 5, 5, 7, 8, 9, 9, 3, 7, 6], i=6

[0, 3, 5, 5, 7, 8, 9, 9, 7, 6], i=7

[0, 3, 5, 5, 7, 7, 8, 9, 9, 6], i=8

[0, 3, 5, 5, 6, 7, 7, 8, 9, 9], i=9

排序后:[0, 3, 5, 5, 6, 7, 7, 8, 9, 9]

希尔排序

希尔排序是对插入排序的一种改进,通过设定一个步长k,将数组从逻辑上拆分为n个小数组,每个小数组内部进行插入排序,将排序的结果对应的原数组中,然后逐步缩小k,直到k=1时完成数组的排序

希尔排序的出现,避免了直接用插入排序便遇到最坏情况的可能,即使一开始分成小数组后,每个数组是最坏情况,但随着每次分成小数组并进行排序后,使得原数组中顺序趋向于最终顺序,趋向于适合插入排序的使用

算法复杂度:希尔排序是一种不稳定的算法,受影响程度在于步长k的设置和调整、底层排序算法的选择。最好情况:O(n*log2n),最坏情况O(n²),平均情况下为O(n1.5)

空间复杂度:O(1)

/**
 * 希尔排序
 * 原理:
 * 希尔排序是插入排序的改进,通过步长k,将大数组在逻辑上分割成几个小数组,小数组中,
 * 每个元素之间的下标间隔为k,对小数组进行插入排序,交换时,下标位置以原大数组的下标
 * 位置为准,然后逐步缩小k的值,直到k=1时,希尔排序变成普通的插入排序
 * 关键点:
 * 注意逻辑上拆分之后的小数组的起始值,自增长度等,即在插入排序中,自增的值由1变为k
 * 算法复杂度:不稳定,因为底层排序是基于插入排序的,通过步长值逐步减小,将大数组排序
 * 分解为小数组排序的算法复杂度为O(log₂n),底层的插入排序的算法复杂度为O(n)到O(n²)
 */
@Test
public void shellSort() {
    if (arr.length <= 1) {
        return;
    }
    int k = arr.length / 2;// 设置步长,通常为长度的一半,具体设置看实际情况
    while (k >= 1) {
        for (int i = 0; i < arr.length; i++) {
            for (int j = i + k; j < arr.length; j += k) {
                for (int l = j; l >= i + k; l -= k) {
                    if (arr[l - k] > arr[l]) {
                        swap(arr, l - k, l);
                    } else {
                        break;
                    }
                }
            }
        }
        System.out.println(Arrays.toString(arr) + ", k=" + String.valueOf(k));
        k /= 2;
    }
}

执行结果如下:

原数组:[5, 8, 2, 2, 1, 0, 5, 4, 5, 8]

[0, 5, 2, 2, 1, 5, 8, 4, 5, 8], k=5

[0, 2, 1, 4, 2, 5, 5, 5, 8, 8], k=2

[0, 1, 2, 2, 4, 5, 5, 5, 8, 8], k=1

排序后:[0, 1, 2, 2, 4, 5, 5, 5, 8, 8]

快速排序

快速排序的原理有点儿类似于二分法,首先从数组中选取一个值作为分界值,然后对数组进行排序比较,使得数组中所有小于分界值的数都排在分界值的左边,数组中所有排在分界值右边的数都排在分界值的右边(等值看排序要求进行分析),这样就将数组分成两部分,左边部分的数总数小于右边部分的数,然后依次类推继续往下进行“分割”直到分割完成后的部分只有一个数,则完成对数组的排序

基于快排的思想,可以看出递归适合该算法的实现,实现如下:

/**
 * 快速排序(从小到大)
 * 原理:
 * 对于一个长度为n(n > 1)的数组,从数组中挑选一个基准值N[k]=K,将数组进行筛选,使得任何小于N[k]的数都放到k的左
 * 边(只要放到k的左边就可以),任何大于N[k]的数都放到k的右边(只要放到k的右边就可以),即令k∈[0, n),
 * 则N[m] < k,m∈[0, n/2)且N[t] > k,t∈(n/2 n-1]
 * 分好之后,以N[0]到N[n/2-1]和N[n/2+1]到N[n-1]为新的小数组,在每个小数组的基础上在进行上述步骤,直到小数
 * 组的长度为1时结束
 * 思路:
 * 就地排序和递归都可以,递归采用空间换取时间的方式,且思路更清晰
 * 算法复杂度:不稳定,和partition的值有关,最坏情况下是O(n²),相当于选择排序,平均时间复杂度是O(nlog₂n)(log₂n
 * 次分隔,每次分隔后都是1次遍历,总共遍历n次)
 * 空间复杂度:不稳定,跟分隔次数有关,从log₂n次到n次
 */
@Test
public void quickSort() {
    if (arr.length <= 1) {
        return;
    }
    partitionAndSort(arr, 0, arr.length - 1);
}

/**
 * 根据选中的基准值进行分类,为了能进行递归操作
 *
 * @param arr 数组
 * @param a   第一个元素的位置
 * @param b   最后一个蒜素的位置
 */
private void partitionAndSort(int[] arr, int a, int b) {
    if (a >= b) {
        return;
    }
    // 从数组中选择一个基准值
    int k = (int) (a + Math.random() * (b - a));
    /*
     * 分类思路如下:
     * 1.将N[k]=K放到数组最有侧,即N[b]=K
     * 2.从N[a]开始,若N[a] < N[b],a自增1,若N[a] > N[b],则将N[a]与N[b-1]交换,
     * b减去1
     * 3.重复上述步骤,直到a不再小于b为止,此时,N[a-1]及其左边的元素都小于K,
     * N[b]及其右边的元素都不小于K,将N[a]与N[b]交换,即得到分类完成数组,此时
     * 左子数组的最后一个元素的下标为a-1,右子数组第一个元素的下标b
     * 4.分别对子数组进行3步骤,直到子数组的长度为1时结束
     */
    int left = a, right = b;
    swap(arr, k, b);
    while (a < b) {
        if (arr[a] < arr[right]) {
            a++;
        } else if (arr[a] > arr[right]) {
            swap(arr, a, --b);
        } else {
            a++;
        }
    }
    // 此时N[a-1]及其左边元素都不大于K,N[a]与K交换后完成分类
    swap(arr, a, right);
    int tmp[] = new int[right - left + 1];
    System.arraycopy(arr, left, tmp, 0, tmp.length);
    System.out.println("分类好的子数组是:" + Arrays.toString(tmp));
    System.out.println("left=" + left + ", right=" + right + ", k=N[" + a + "]=" + arr[a]);
    System.out.println("目前数组的状况是:" + Arrays.toString(arr));
    System.out.println("-----------------------------------------------");
    partitionAndSort(arr, left, a - 1);
    partitionAndSort(arr, a + 1, right);
}

执行结果如下:

原数组:[0, 2, 6, 0, 4, 4, 7, 3, 7, 1]

分类好的子数组是:[0, 2, 3, 0, 1, 4, 4, 7, 6, 7]

left=0, right=9, k=N[6]=4

目前数组的状况是:[0, 2, 3, 0, 1, 4, 4, 7, 6, 7]

-----------------------------------------------

分类好的子数组是:[0, 0, 4, 1, 2, 3]

left=0, right=5, k=N[1]=0

目前数组的状况是:[0, 0, 4, 1, 2, 3, 4, 7, 6, 7]

-----------------------------------------------

分类好的子数组是:[1, 2, 4, 3]

left=2, right=5, k=N[2]=1

目前数组的状况是:[0, 0, 1, 2, 4, 3, 4, 7, 6, 7]

-----------------------------------------------

分类好的子数组是:[2, 3, 4]

left=3, right=5, k=N[3]=2

目前数组的状况是:[0, 0, 1, 2, 3, 4, 4, 7, 6, 7]

-----------------------------------------------

分类好的子数组是:[3, 4]

left=4, right=5, k=N[4]=3

目前数组的状况是:[0, 0, 1, 2, 3, 4, 4, 7, 6, 7]

-----------------------------------------------

分类好的子数组是:[6, 7, 7]

left=7, right=9, k=N[7]=6

目前数组的状况是:[0, 0, 1, 2, 3, 4, 4, 6, 7, 7]

-----------------------------------------------

分类好的子数组是:[7, 7]

left=8, right=9, k=N[9]=7

目前数组的状况是:[0, 0, 1, 2, 3, 4, 4, 6, 7, 7]

-----------------------------------------------

排序后:[0, 0, 1, 2, 3, 4, 4, 6, 7, 7]

归并排序

归并排序主要利用分治法的思想,将一个大的数组的排序过程转换成无数个小数组排序合并的过程。将一个数组进行平分,平分之后的数组再进行平分,一直平分下去,知道数组不可平分,然后将这些数组进行比较合并,合成一个有序数组

两个数组比较合并时,需要创建一个新数组存放两个数组的合集,两个数组各自遍历并将值比较,值较小或者较大的数放入新数组,较大的数与较小的数所在的数组的下一个数进行比较,当一个数组遍历完成时,另一个数组中未被遍历的数直接按顺序添加到新数组中

算法复杂度:O(n*log2n),进行了log2n次分隔操作,每次合并时只进行一次遍历

空间复杂度:O(n),每次合并都需要一个新数组存放两个数组的排序合并结果,这个数组最长为原数组长度n

/**
 * 归并排序
 * 原理:核心思想是分治法
 * 将一个大的数组进行中间分隔,剩下的小数组每个再进行中间分隔,知道所有的子数组都不可分隔为止
 * 然后从左往右开始,每两个最小数组(长度为1)开始合并成一个新数组,两个最小数组用同一个变量进
 * 行遍历,遍历时,两个数组中遍历的最小的元素取出放入新数组,其中一个数组遍历结束时,剩下的未
 * 遍历完的数组依次取出并放入新数组,依次类推,直至两个子数组合并后形成完整的数组
 * <p>
 * 归并排序同样有递归和循环两种实现方式
 * 算法复杂度:O(nlog₂n)
 */
@Test
public void mergeSort() {
    if (arr.length <= 1) {
        return;
    }
    arr = mergeSort(arr, 0, arr.length - 1);
}

/**
 * 分割
 *
 * @param arr   数组
 * @param start 起始位置
 * @param end   末尾位置
 */
private int[] mergeSort(int[] arr, int start, int end) {
    System.out.println("start=" + start + ", end=" + end);
    if (start == end) {
        return new int[]{arr[start]};
    }
    int k = (end + start) / 2;
    // 每个数组从中间分割成两个子数组
    int[] left = mergeSort(arr, start, k);
    int[] right = mergeSort(arr, k + 1, end);
    // 当两个子数组不可再分割时,两个子数组合并后返回
    int len = Math.min(left.length, right.length);
    int i_left = 0, i_right = 0, i_merge = 0;
    // 对两个子数组同时遍历,两个元素中,较小的元素追加到新数组
    int[] merge = new int[left.length + right.length];
    while (i_left < len && i_right < len) {
        merge[i_merge++] = left[i_left] < right[i_right] ? left[i_left++] : right[i_right++];
    }
    // 剩下的未遍历完数组,剩下的元素追加到新数组后边
    while (i_left < left.length) {
        merge[i_merge++] = left[i_left++];
    }
    while (i_right < right.length) {
        merge[i_merge++] = right[i_right++];
    }
    return merge;
}

执行效果如下:

原数组:[1, 6, 6, 6, 4, 7, 3, 4, 2, 8]

start=0, end=9

start=0, end=4

start=0, end=2

start=0, end=1

start=0, end=0

start=1, end=1

start=2, end=2

start=3, end=4

start=3, end=3

start=4, end=4

start=5, end=9

start=5, end=7

start=5, end=6

start=5, end=5

start=6, end=6

start=7, end=7

start=8, end=9

start=8, end=8

start=9, end=9

排序后:[1, 2, 3, 4, 6, 6, 6, 7, 4, 8]

 

归并排序也可以利用循环来实现,使用的数据结构是栈,使用两个栈来进行辅助排序,先利用栈1进行数组的分割操作,然后循环从栈1作出栈操作,若分割的数组不彻底,则将数组分割然后入栈,若数组不可再分割,则放入栈2,这样直至栈1为空栈时,栈2中存的都是不可再分的数组,即长度为1的数组,然后准备进行合并操作

将栈1和栈2的指针交换,即每次作合并操作时都是栈1出栈,栈2入栈,即将栈1中的数组合并后放入栈2,这样循环交替操作后,直至栈1的长度为1,栈2为空时,栈1中的数组即为原数组对应的排序数组

这里之所以用两个栈而不是一个栈作排序是因为一个栈作排序在合并过程中会出现小数组与大数组合并的情况,即新数组中,比较的部分长度所占比例过小,排序效率不高,所以采用两个栈交替出入栈合并来提高排序效率

/**
 * 归并排序(循环方式实现)
 * 思路:归并排序同样可以用循环实现,同样使用栈来存放每个子数组的范围
 * 但是合并的时候,如果只使用栈,会造成合并时,两个数组的长度差距过大,
 * 所以这里建议使用两个栈,利用栈1将大数组分割成最小数组,在出栈1时对
 * 出栈的子数组的范围进行判断,只要不是最小数组,就压入栈,反之,取出,
 * 放入栈2中,这样,栈1和栈2交替出栈并进栈,就可以将数组从小到大合并,
 * 也不会出现合并时,两个数组差距过大的情况,以此类推,直到栈1和栈2的
 * 深度之和为1时,栈中的唯一的数组就是排序好的数组
 */
@Test
public void mergeSort1() {
    if (arr.length <= 1) {
        return;
    }
    Stack<int[]> stack1 = new Stack<int[]>();
    Stack<int[]> stack2 = new Stack<int[]>();
    int[] scope = new int[]{0, arr.length - 1};
    stack1.push(scope);
    int start, end, k;
    // 利用栈1进行数组平分,并将原数组分割成的最小数组都放到栈2中
    while (!stack1.isEmpty()) {
        scope = stack1.pop();
        start = scope[0];
        end = scope[1];
        // 只要数组可再平分,就压栈,反之就放到队列中
        if (start != end) {
            k = (start + end) / 2;
            stack1.push(new int[]{start, k});
            stack1.push(new int[]{k + 1, end});
        } else {
            stack2.add(new int[]{arr[start]});
        }
    }
    /*
     * 更改变量定义,stack1用来出栈,stack2用来入栈,从stack1出栈
     * 每次取两个子数组合并,将合并后的数组放入stack2,若最后只取出一个子数组,
     * 则直接放入stack2,然后两个变量引用互换总是让stack1出栈stack2入栈,
     * 直至stack1和stack2中有一个是空栈且两个栈的深度之和为1时,栈中的数组就是
     * 排序好的数组
     */
    // 用于两个栈的引用交换
    Stack<int[]> tmp = stack1;
    stack1 = stack2;
    stack2 = tmp;
    int[] tmp1 = null, tmp2 = null, merge = null;
    int i_a, i_b, i, len;
    while (stack1.size() + stack2.size() != 1) {
        while (!stack1.isEmpty()) {
            scope = stack1.pop();
            if (tmp1 == null) {
                tmp1 = scope;
                continue;
            } else {
                tmp2 = scope;
            }
            // 当取出两个子数组时,两个子数组合并成新数组压入stack2
            i_a = 0;
            i_b = 0;
            i = 0;
            len = tmp1.length + tmp2.length;
            merge = new int[len];
            while (i_a < tmp1.length && i_b < tmp2.length) {
                merge[i++] = tmp1[i_a] < tmp2[i_b] ? tmp1[i_a++] : tmp2[i_b++];
            }
            while (i_a < tmp1.length) {
                merge[i++] = tmp1[i_a++];
            }
            while (i_b < tmp2.length) {
                merge[i++] = tmp2[i_b++];
            }
            stack2.push(merge);
            tmp1 = null;
            tmp2 = null;
        }
        // 若stack1中只剩下一个子数组时,直接压入stack2
        if (tmp1 != null && tmp2 == null) {
            stack2.push(tmp1);
            tmp1 = null;
        }
        tmp = stack1;
        stack1 = stack2;
        stack2 = tmp;
    }
    arr = stack1.pop();
}

执行效果如下:

原数组:[7, 9, 7, 4, 3, 5, 9, 3, 8, 4]

排序后:[3, 3, 4, 4, 5, 7, 7, 8, 9, 9]

 

其实上边的排序方法可以继续改进,即将原数组直接分成n个长度为1的数组,然后进行上边方法的合并步骤,可以简化数组分割部分的步骤,提高排序效率

/**
 * 归并排序(改进循环实现)
 * 其实到这里也明白了,根据归并排序的思想,其实就是将一个大数组
 * 通过分解,分解成有限个最小数组,通过最小数组两两排序合并,最终
 * 达到排序大数组的目标,所以利用循环去实现,我们可以直接将大数组遍历
 * 将大数组直接分解成长度为1的数组,然后按照mergeSort1中通过两个栈
 * 交替出栈入栈,进行数组的排序合并
 */
@Test
public void mergeSort2() {
    if (arr.length <= 1) {
        return;
    }
    Stack<int[]> stack1 = new Stack<>();
    Stack<int[]> stack2 = new Stack<>();
    Stack<int[]> tmp;
    for (int i = 0; i < arr.length; i++) {
        stack1.push(new int[]{arr[i]});
    }
    int[] scope, tmp1 = null, tmp2 = null, merge;
    int i_a, i_b, i, len;
    while (stack1.size() + stack2.size() != 1) {
        while (!stack1.isEmpty()) {
            scope = stack1.pop();
            if (tmp1 == null) {
                tmp1 = scope;
                continue;
            } else {
                tmp2 = scope;
            }
            // 当取出两个子数组时,两个子数组合并成新数组压入stack2
            i_a = 0;
            i_b = 0;
            i = 0;
            len = tmp1.length + tmp2.length;
            merge = new int[len];
            while (i_a < tmp1.length && i_b < tmp2.length) {
                merge[i++] = tmp1[i_a] < tmp2[i_b] ? tmp1[i_a++] : tmp2[i_b++];
            }
            while (i_a < tmp1.length) {
                merge[i++] = tmp1[i_a++];
            }
            while (i_b < tmp2.length) {
                merge[i++] = tmp2[i_b++];
            }
            stack2.push(merge);
            tmp1 = null;
            tmp2 = null;
        }
        // 若stack1中只剩下一个子数组时,直接压入stack2
        if (tmp1 != null && tmp2 == null) {
            stack2.push(tmp1);
            tmp1 = null;
        }
        tmp = stack1;
        stack1 = stack2;
        stack2 = tmp;
    }
    arr = stack1.pop();
}

执行结果如下:

原数组:[5, 8, 2, 1, 8, 5, 3, 5, 9, 4]

排序后:[1, 2, 3, 4, 5, 5, 5, 8, 8, 9]

堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

利用堆这种性质,可以将数组放入堆中,构成大顶堆(任意的子节点,其值总是小于它的祖先节点),通过反复的弹出堆的根节点和重新组成大顶堆,可将数组进行排序

算法复杂度:O(n*log2n),弹出堆的根节点n次,重构大顶堆时,作log2n次节点交换

空间复杂度:O(n),需要一个长度和原数组相同的数组来实现大顶堆

/**
 * 堆排序(从小到大)
 * 原理:
 * 堆是一种特殊的二叉树,满足性质:任意子节点不小于(或者不大于)它的所有祖先节点,
 * 因此堆又可以分为大顶堆和小顶堆,大顶堆要求任意子节点不大于其所有祖先节点,小顶堆要求
 * 任意子节点不小于其所有祖先节点,至于某一节点的左右子节点有何种关系没有影响
 * <p>
 * 基于这种特殊性质,可以利用堆来进行排序,堆可以利用数组来实现
 * <p>
 * 思路:
 * 1.将数组(长度为n)中元素填充到堆中构成大顶堆N
 * 2.由于堆是由数组构成的,所以交换N[0]与N[n-1],则N[n-1]为数组中最大的数,排在最右侧,此时堆的节点数-1
 * 3.将数组从下标0到n-2重新构成大顶堆,即堆的逻辑节点数-1,再将N[0]与N[n-2]交换,直到逻辑上的堆的深度
 * 为1时结束
 */
@Test
public void heapSort() {
    if (arr.length <= 1) {
        return;
    }
    // 构建大顶堆
    int[] heap = new int[arr.length];
    int parentIndex, j;
    for (int i = 1; i < arr.length; i++) {
        heap[i] = arr[i];
        j = i;
        while (true) {
            parentIndex = (j - 1) / 2;// 父节点下标
            if (parentIndex < 0) {// 若该节点没有父节点
                break;
            }
            // 若该节点有父节点,则进行比较和交换
            if (heap[parentIndex] < heap[j]) {
                swap(heap, parentIndex, j);
                j = parentIndex;
            } else {
                break;
            }
        }
    }
    System.out.println("大顶堆:" + Arrays.toString(heap));
    int len = heap.length, leftIndex, rightIndex, maxIndex;
    while (true) {
        swap(heap, 0, --len);
        if (len == 1) {
            break;
        }
        j = 0;
        while (true) {
            leftIndex = 2 * j + 1;// 左子节点下标
            rightIndex = 2 * j + 2;// 右子节点下标
            if (leftIndex >= len) {// 该节点没有子节点
                break;
            }
            /*
             * 该节点有子节点,判断该节点与子节点中的最大值,若该节点最大则完成构建堆
             * 若该节点不是最大值,则该节点与最大值节点交换,且指针指向原最大值节点的
             * 下标继续判断,直到该节点没有子节点为止
             */
            maxIndex = j;
            if (heap[maxIndex] < heap[leftIndex]) {
                maxIndex = leftIndex;
            }
            if (rightIndex < len && heap[maxIndex] < heap[rightIndex]) {
                maxIndex = rightIndex;
            }
            if (maxIndex != j) {
                swap(heap, j, maxIndex);
                j = maxIndex;
            } else {
                break;
            }
        }
    }
    arr = heap;
}

执行结果如下:

原数组:[0, 6, 4, 7, 8, 2, 5, 8, 7, 3]

大顶堆:[8, 8, 5, 7, 6, 2, 4, 0, 7, 3]

排序后:[0, 2, 3, 4, 5, 6, 7, 7, 8, 8]

计数排序

计数排序,顾名思义,统计数组中每个元素出现的次数,表示在另一个数组,例如:原数组中N[i]在数组中出现的次数为k,则在计数数组中表示为M[N[i]]=k,统计完成之后,根据计数数组中的统计结果,即可将数组进行排序

需要注意的是使用计数排序是,数组中最小值和最大值的差距会影响数组的长度,差距越大,计数数组的长度越大,占用空间越大,所以计数数组适合用来排序一些数组内元素的差距跨度不是很大的数组,且计数数组只适合排序整数

算法复杂度:计数排序是一种稳定的线性时间排序算法,复杂度O(n+k),k表示数组中众数出现的次数

空间复杂度:O(n+k)

其实关于计数排序的空间复杂度,有说O(n+k)的,也有说O(k),我个人更倾向于O(k),这里的k表示的是数组中的值域范围,即用一个长度为k的计数数组,就能将一个长度为n的数组中,从最小值到最大值的所有元素都统计到,算法实现如下:

/**
 * 计数排序
 * 原理:
 * 用一个数组M来统计需要排序的数组N中,每个元素出现的次数,比如M[i]=t,表示
 * 在数组N中,值为i的元素出现了t次,统计之后,就可以通过数组M和其中的信息,
 * 将数组N进行排序
 * <p>
 * 使用计数排序时,原数组中最大最小值的差距会影响计数数组的长度,所以计数排序
 * 适合用来排序一些数字跨度不太大的数组,注意,计数排序只适合排序整数
 *
 * 算法复杂度:计数排序是一种稳定的线性时间排序算法,复杂度为O(n+k)
 * 空间复杂度:O(n+k)
 */
@Test
public void countSort() {
    if (arr.length <= 1) {
        return;
    }
    int min = arr[0], max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] < min) {
            min = arr[i];
        }
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    // 在计数数组中,从0到n-1代表原数组从min到max
    int[] countArray = new int[max - min + 1];
    for (int i = 0; i < arr.length; i++) {
        countArray[arr[i] - min]++;
    }
    // 用计数数组将原数组排序
    int index = 0;
    for (int i = 0; i < countArray.length; i++) {
        for (int j = 0; j < countArray[i]; j++) {
            arr[index++] = i + min;
        }
    }
}

执行结果如下:

原数组:[2, 4, 7, 6, 6, 4, 2, 2, 3, 3]

排序后:[2, 2, 2, 3, 3, 4, 4, 6, 6, 7]

基数排序

基数排序是一种基于计数排序的改进算法,毕竟有时候我们要排序的数组,其中的元素是不确定的,那么其值域也就无法确定,这样我们用计数排序去排序数组时,所占用的空间是无法保证的。而基数排序是将数组中每一个数,从高位到低位(或者从低位到高位)进行排序并统计,从在统计数组中有值大于1的情况(即有多个数某个数位上的数相同),则递归堆这几个数再进行排序统计,以此类推,完成对数组的排序

算法复杂度:O(d(n+k)),n表示数组长度,k表示计数数组中的最大值,d表示数组中至少两个数中,从高位到低位(或者从低位到高位)开始,数位上的数连续相同的数位的个数,即递归的深度

空间复杂度:O(n),算法实现中需要一个二维数组用来统计与计数数组相对应的原数组中的元素,其中第一维度的长度为10(单个数位上0-9),第二维度长度为原数组长度,保证这个二维数组对于原数组中的数据都能统计到

/**
 * 基数排序(从小到大)
 * 原理:
 * 基数排序是指将一个数组中的元素,按照低位到高位或者高位到低位依次排序
 * 排序完成后该数组即变成有序数组
 *
 * 算法复杂度:O(k*n)
 * 空间复杂度:O(n)
 */
@Test
public void radixSort() {
    if (arr.length <= 1) {
        return;
    }
    // 从高位到高位
    // 找出最大数共有几个数位
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (max < arr[i]) {
            max = arr[i];
        }
    }
    int digit;// 最高位
    if (max == 0) {
        digit = 1;
    } else {
        digit = (int) (Math.log10(max)) + 1;
    }
    radixSort(arr, digit);
}

private void radixSort(int[] arr, int digit) {
    int[][] bucket = new int[10][arr.length];
    int[] countArray = new int[10];
    /*
     * 从高位到低位,按照元素中某一个数位上的数进行计数排序
     * 并将排序号的顺序填充到原数组中
     */
    int tmp, index = 0;
    // 对应数位上的数作为下标,填充到下标对应的数组中
    for (int i = 0; i < arr.length; i++) {
        tmp = getByDigit(arr[i], digit);
        bucket[tmp][countArray[tmp]] = arr[i];
        // 同时计数数组中作记录
        countArray[tmp]++;
    }
    // 根据计数数组中的记录,重新排序原数组
    for (int i = 0; i < countArray.length; i++) {
        tmp = countArray[i];// 计数数组中对应bucket的子数组中存的元素个数
        // 若计数数组中元素为1,即bucket对应的数组中只有一个数,则填充到原数组中
        if (tmp == 0) {
            continue;
        } else if (tmp == 1) {
            arr[index++] = bucket[i][0];
        } else {// 若有多个数,则这几个数递归进行排序
            int[] sub = new int[tmp];
            System.arraycopy(bucket[i], 0, sub, 0, tmp);
            radixSort(sub, digit - 1);
            for (int j = 0; j < sub.length; j++) {
                arr[index++] = sub[j];
            }
        }
    }
    System.out.println(Arrays.toString(arr));
}

/**
 * 获取某一数位上的数
 * @return
 */
private int getByDigit(int i, int digit) {
    if (i == 0) {
        return 0;
    } else {
        int big = (int) Math.pow(10, digit);
        int small = (int) Math.pow(10, digit - 1);
        return i % big / small;
    }
}

执行结果如下:

原数组:[55, 78, 99, 45, 43, 72, 74, 14, 71, 58]

[43, 45]

[55, 58]

[71, 72, 74, 78]

[14, 43, 45, 55, 58, 71, 72, 74, 78, 99]

排序后:[14, 43, 45, 55, 58, 71, 72, 74, 78, 99]

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值