算法学习

84 阅读7分钟

算法学习

二分查找

算法描述

  1. 前提:有已排序数组 A(假设已经做好)

  2. 定义左边界 L、右边界 R,确定搜索范围,循环执⾏⼆分查找(3、4两步)

  3. 获取中间索引 M = Floor((L+R) /2)

  4. 中间索引的值 A[M] 与待搜索的值 T 进行比较

​ ① A[M] == T 表示找到,返回中间索引

​ ② A[M] > T,中间值右侧的其它元素都⼤于 T,⽆需⽐较,中间索引左边去找,M - 1 设置为右边界,重新查找

​ ③ A[M] < T,中间值左侧的其它元素都⼩于 T,⽆需⽐较,中间索引右边去找, M + 1设置为左边界,重新查找

  1. 当 L > R 时,表示没有找到,应结束循环

算法实现

public class BinarySearch {
        public static void main(String[] args) {
        // 1.定义有序的int类型数组
        int[] array = {24, 33, 45, 56, 58, 62, 74, 89, 102};
        // 2.定义需要查找的对象
        int target = 74;
        // 3.调⽤⼆分查找⽅法函数 获得对象在数组中索引值
        int index = binarySearch(array, target);
        // 4.输出结果
        System.out.println("index = " + index);
    }

    private static int binarySearch(int[] array, int target) {
        // 1.定义左右边界和中间值
        int left = 0, right = array.length - 1, m = 0;
        // 2.循环执⾏⼆分查找
        while (left <= right) {
            // 避免int整数相加溢出 更换减法 left/2 + right/2 ==>
            //left + (-left / 2 + right / 2) ==>left + (right - left) / 2
            //m = (left + right) / 2;
            //m = left + (right - left) / 2;
            // 采⽤位运算右移1位则代表除以2
            m = (left + right) >>> 1;
            // 3.判断查找对象和遍历的当前值
            if (array[m] == target) {
                return m;
            } else if (array[m] > target) {
                right = m - 1;
            } else {
                left = m + 1;
            }
        }
        return -1;
    }
}

记得⼀个简要判断⼝诀:奇数⼆分取中间,偶数⼆分取中间靠左。

冒泡排序

算法描述

  1. 依次⽐较数组中相邻两个元素⼤⼩,若 a[j] > a[j+1],则交换两个元素,两两都⽐较⼀遍

称为⼀轮冒泡,结果是让最⼤的元素排⾄最后

  1. 重复以上步骤,直到整个数组有序

算法实现

    /**
     * 通过判断当前是否发⽣变化从⽽优化算法
     * 
     * 优化点1:每经过⼀轮冒泡,内层循环就可以减少⼀次
     * 优化点2:如果某⼀轮冒泡没有发⽣交换,则表示所有数据有序,可以结束外层循环
     * 优化算法实现
     *
     * @param array
     */
    private static void bubble(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean swapped = false;
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    swap(array, j, j + 1);
                    swapped = true;
                }
            }
            System.out.println("第" + (i + 1) + "轮数组 = " +
                    Arrays.toString(array));
            if (!swapped) {
                break;
            }
        }
    }
  • 优化点1:每经过⼀轮冒泡,内层循环就可以减少⼀次

  • 优化点2:如果某⼀轮冒泡没有发⽣交换,则表示所有数据有序,可以结束外层循环

优化算法实现

/**
     * 通过判断最后⼀次交换值是否发送改变
     * 从⽽判断最后⼀次被交换的索引⾄最后⼀位已有序 进⽽优化算法
     *
     * @param array
     */
    private static void bubble(int[] array) {
        int swapIndex = array.length - 1;
        do {
            // 1.进⼊循环⾄0
            int last = 0;
            for (int i = 0; i < swapIndex; i++) {
                if (array[i] > array[i + 1]) {
                    swap(array, i, i + 1);
            // 2.记录最后⼀次交换值的索引
                    last = i;
                }
            }
            // 3.将最后⼀次交换的索引值赋值 作为下⼀次排序遍历的⻓度
            swapIndex = last;
            // 4.判断当前是否已经交换完 形成有序数组
            System.out.println("Arrays.toString(array) = " +
                    Arrays.toString(array));
        } while (swapIndex != 0);
    }
  • 每轮冒泡时,最后⼀次交换索引可以作为下⼀轮冒泡的⽐较次数,如果这个值为零,表示

    整个数组有序,直接退出外层循环即可

选择排序

算法描述

  1. 将数组分为两个⼦集,排序的和未排序的,每⼀轮从未排序的⼦集中选出最⼩的元素,放

⼊排序⼦集

  1. 重复以上步骤,直到整个数组有序

算法实现

    /**
     * 选择排序-算法实现
     *
     * @param array
     */
    private static void selection(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            // i代表每轮排序最⼩值需要交换到的具体索引
            // selectIndex代表每轮已被选择的值当前在数据中的具体索引
            int selectIndex = i;
            for (int j = selectIndex + 1; j < array.length; j++) {
                // 遍历(选择)剩余未被排序的其他数组
                if (array[selectIndex] > array[j]) {
                    // 记录当前该轮出现最⼩值
                    selectIndex = j;
                    // 当争论执⾏完再进⾏交换 减少交换次数
                    //swap(array, i, j);
                }
            }
            if (selectIndex != i) {
                swap(array, selectIndex, i);
            }
            System.out.println("Arrays.toString(array) = " +
                    Arrays.toString(array));
        }
    }

    public static void main(String[] args) {
        // 1.定义有序的int类型数组
        int[] array = {54, 33, 45, 56, 58, 22, 64, 89, 12};

        // 2.调⽤选择排序⽅法函数
        selection(array);
    }

与冒泡排序⽐较

  1. ⼆者平均时间复杂度都是 O(n^2)

  2. 选择排序⼀般要快于冒泡,因为其交换次数少

  3. 但如果集合有序度⾼,冒泡优于选择

  4. 冒泡属于稳定排序算法,⽽选择属于不稳定排序

    1. 稳定排序指,按对象中不同字段进⾏多次排序,不会打乱同值元素的顺序

    不稳定排序则反之

  5. 选择排序和冒泡排序的时间复杂度都是O(n^2),这意味着当处理大规模数据时,它们的效率可能较低。这两种排序算法通常用于教学目的,以及处理小规模或部分已排序的数据集。对于大规模数据集,更高效的排序算法(如快速排序、归并排序等)通常是更好的选择。

插入排序

算法描述

  1. 将数组分为两个区域,排序区域和未排序区域,每⼀轮从未排序区域中取出第⼀个元素,插⼊到排序区域(需保证顺序)

  2. 重复以上步骤,直到整个数组有序


    /**
     * 插⼊排序-算法实现
     *
     * @param array
     */
    private static void insertSort(int[] array) {
        // 当前i表示需要进⾏插⼊的元素的索引值
        for (int i = 1; i < array.length; i++) {
            // 通过临时变量存储需要向前插⼊元素的值
            int temp = array[i];
            // 通过i值找到与之前⼀位的索引值
            int front = i - 1;
            while (front >= 0) {
                if (temp < array[front]) {
                    // 若需要插⼊元素值⼩于前⼀位元素值 较⼤值后移
                    array[front + 1] = array[front];
                } else {
                    // 已找到需要插⼊的位置索引值 退出循环
                    break;
                }
                front--;
            }
            // 向⽐较索引值的后⼀位进⾏插⼊操作
            array[front + 1] = temp;
            System.out.println("Arrays.toString(array) = " +
                    Arrays.toString(array));
        }
    }

    public static void main(String[] args) {
        // 1.定义有序的int类型数组
        int[] array = {54, 33, 45, 56, 58, 22, 64, 89, 12};
        // 2.调⽤
        insertSort(array);
    }

与选择排序⽐较

  1. 二者平均时间复杂度都是 O(n^2)

  2. 大部分情况下,插⼊都略优于选择(选择略优于冒泡)

  3. 有序集合插⼊的时间复杂度为 O(n)

  4. 插⼊属于稳定排序算法,⽽选择属于不稳定排序

警示

插⼊排序易被轻视,其实它的地位非常重要。小数据量排序,都会优先选择插⼊排序

希尔排序

算法描述

  1. 首先选取⼀个间隙序列,如 (n/2,n/4 … 1),n 为数组⻓度

  2. 每⼀轮将间隙相等的元素视为⼀组,对组内元素进⾏插⼊排序,⽬的有⼆

​ ① 少量元素插⼊排序速度很快

​ ② 让组内值较⼤的元素更快地移动到后⽅

  1. 当间隙逐渐减少,直⾄为 1 时,即可完成排序
private static void shellSort(int[] a) {
        int n = a.length;
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // i 代表待插⼊元素的索引
            for (int i = gap; i < n; i++) {
                int t = a[i]; // 代表待插⼊的元素值
                int j = i;
                while (j >= gap) {
                    // 每次与上⼀个间隙为 gap 的元素进⾏插⼊排序
                    if (t < a[j - gap]) { // j-gap 是上⼀个元素索引,如果 > t,后移
                        a[j] = a[j - gap];
                        j -= gap;
                    } else { // 如果 j-1 已经 <= t, 则 j 就是插⼊位置
                        break;
                    }
                }
                a[j] = t;
                System.out.println(Arrays.toString(a) + " gap:" + gap);
            }
        }
    }

    //优化
    public static void shellSort(int[] array) {
        int n = array.length;
        int gap = n / 2; // 初始间隔为数组长度的一半

        // 逐步缩小间隔
        while (gap > 0) {
            // 对每个子序列进行插入排序
            for (int i = gap; i < n; i++) {
                int temp = array[i];
                int j;
                // j-gap 是上⼀个元素索引,如果 > t,后移
                for (j = i; j >= gap && array[j - gap] > temp; j -= gap) {
                    array[j] = array[j - gap];
                }
                array[j] = temp;
            }
            gap /= 2; // 缩小间隔
        }
    }

    public static void main(String[] args) {
        int[] array = {9, 8, 3, 7, 5, 1, 4, 6, 2, 0};
        shellSort(array);
        for (int i : array) {
            System.out.print(i + " ");
        }
    }

希尔排序是对插入排序的一种改进,在效率上较直接插入、冒泡、选择排序方法有较大改进。它适用于中等规模的数据排序,是冲破O(n^2)的第一批算法之一

快速排序

算法描述

  1. 每⼀轮排序选择⼀个基准点(pivot)进⾏分区
  • 让⼩于基准点的元素的进⼊⼀个分区,⼤于基准点的元素的进⼊另⼀个分区

  • 当分区完成时,基准点元素的位置就是其最终位置

  1. 在⼦分区内重复以上过程,直⾄⼦分区元素个数少于等于 1,这体现的是分⽽治之的思想(divide-and-conquer)

  2. 从以上描述可以看出,⼀个关键在于分区算法,常⻅的有洛穆托分区⽅案、双边循环分区方案、霍尔分区⽅案

public static void main(String[] args) {
        int[] array = {9, 8, 3, 7, 5, 1, 4, 6, 2, 0};
        quickSort(array, 0, array.length - 1);
        for (int i : array) {
            System.out.print(i + " ");
        }
    }

    public static void quickSort(int[] array, int low, int high) {
        if (low < high) {
            int pivotIndex = partition(array, low, high);
            quickSort(array, low, pivotIndex - 1);
            quickSort(array, pivotIndex + 1, high);
        }
    }

    public static int partition(int[] array, int low, int high) {
        int pivot = array[high]; // 选择最后一个元素作为基准
        int i = (low - 1); // 小于基准的元素索引

        for (int j = low; j <= high - 1; j++) {
            if (array[j] < pivot) {
                i++;
                swap(array, i, j);
            }
        }
        swap(array, i + 1, high); // 将基准元素放到正确的位置
        return i + 1;
    }

    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

它的基本思想是:通过一次排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序的算法实现通常包括一个基准元素(pivot)的选择和分区(partitioning)过程