【✜算法】算法

十大经典排序算法

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制和规范,要得到一个符合实际的优秀算法,得经过大量的推理和分析。

排序算法可以分为:

  • 内部排序数据记录在内存中进行排序。
  • 外部排序因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等,本文只讲解内部排序算法。用一张图概括:

名词解释

  • n:数据规模
  • k:"桶"的个数
  • in-place:占用常数内存,不占用额外内存。例如:在冒泡排序中,为了将数组排序,借用了一个temp的临时变量,开辟了一个临时空间,这个空间是常数量,这就是in-place。
  • Out-place:占用额外内存。如果开辟的辅助空间与问题规模有关,则是out-place。假设排序时把数组中的数按顺序放入了一个新的数组,这就开了一个n规模大小的数组,这个就与数据规模有关。

术语说明

  • 稳定:如果 A 原本在 B 前面,而 A=B,排序之后 A 仍然在 B 的前面。
  • 不稳定:如果 A 原本在 B 的前面,而 A=B,排序之后 A 可能会出现在 B 的后面。
  • 内排序:所有排序操作都在内存中完成。
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
  • 时间复杂度:定性描述一个算法执行所耗费的时间。
  • 空间复杂度:定性描述一个算法执行所需内存的大小。

十种常见排序算法可以分类两大类别:比较类排序非比较类排序

比较类排序是通过比较来决定元素间的相对次序。比较类排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

非比较排序不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

冒泡排序 (Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地遍历要排序的序列,依次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历序列的工作是重复地进行直到没有再需要交换为止,此时说明该序列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢 “浮” 到数列的顶端。

算法步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤 1~3,直到排序完成。

图解算法

代码实现

public class BubbleSortMain {

    public static void main(String[] args) {
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28};
        for (int i = 1; i < arr.length; i++) { //从1开始,总共执行length-1趟
            for (int j = 0; j < arr.length - i; j++) { //从下标0开始, 每执行完一趟,待排序的数逐渐减少(-i)
                if (arr[j] > arr[j + 1]) { //前后两个数做比较
                    int temp = arr[j]; //因为要交换位置,所以需要使用临时变量temp临时保存其中一个数
                    arr[j] = arr[j + 1];  //从小到大排序
                    arr[j + 1] = temp;
                }
            }
            System.out.println("i=" + i + ":" + Arrays.toString(arr));
        }
        System.out.println("最终结果:" + Arrays.toString(arr));
    }
}

算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(n) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)
  • 排序方式 :In-place

选择排序 (Selection Sort)

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第 2 步,直到所有元素均排序完毕。

图解算法

代码实现

public class SelectionSortMain {

    public static void main(String[] args) {
        //选择排序:选择最小(最大)的排在起始位置或者末尾位置
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28};
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;
            for (int j = i + 1; j <= arr.length - 1; j++) { //选择一个元素与其他元素逐一进行比较,得到最小值的下标
                if (arr[minIndex] > arr[j]) {
                    minIndex = j;
                }
            }
            if (minIndex != i) { //如果其他元素中存在最小值的,则最小值与当前元素进行位置交换
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
            System.out.println("i=" + i + ":" + Arrays.toString(arr));
        }
        System.out.println("最终结果:" + Arrays.toString(arr));
    }
}

算法分析

  • 稳定性:不稳定
  • 时间复杂度 :最佳:O(n2) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)
  • 排序方式 :In-place

插入排序 (Insertion Sort)

插入排序是一种简单直观的排序算法。

它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间(只要打过扑克牌的人都应该能够秒懂)。

插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

算法步骤

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤 2~5。

图解算法

代码实现

public class InsertionSortMain {
    public static void main(String[] args) {
        //插入排序:未排序的元素依次与前面已排序的每个元素进行比较,确定其插入的位置
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28};
        for (int i = 1; i < arr.length; i++) {
            int preIndex = i - 1; //已排好序的元素的最大下标
            int current = arr[i]; //当前元素
            while (preIndex >= 0 && current < arr[preIndex]) { //当未排序的当前元素与前面已排序的元素逐一比较, 确定在哪个元素(preIndex)之后
                arr[preIndex + 1] = arr[preIndex]; //如果当前元素比之前已排好序的元素小,其余元素都需要顺序往后移动
                preIndex -= 1;
            }
            arr[preIndex + 1] = current; //在对应的下标设置当前元素
            System.out.println("i=" + i + ":" + Arrays.toString(arr));
        }
        System.out.println("最终结果:" + Arrays.toString(arr));
    }
}
  • 选择一个增量序列 {t1, t2, …, tk},其中 (ti>tj, i<j, tk=1);
  • 按增量序列个数 k,对序列进行 k 趟排序;
  • 每趟排序,根据对应的增量 t,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

代码实现

public class ShellSortMain {
    public static void main(String[] args) {
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28, 17};
        int n = arr.length;
        int gap = n / 2; //分成多少个序列
        while (gap > 0) {
            for (int i = gap; i < n; i++) {
                int current = arr[i]; //当前值
                int preIndex = i - gap; //上一个值的下标
                while (preIndex >= 0 && arr[preIndex] > current) { //使用插入排序, 对每个序列内的元素进行比较自排序
                    arr[preIndex + gap] = arr[preIndex];
                    preIndex -= gap;
                }
                arr[preIndex + gap] = current; //设置当前值到指定位置
            }
            System.out.println("gap=" + gap + ":" + Arrays.toString(arr));
            gap /= 2;
        }
        System.out.println("最终结果:" + Arrays.toString(arr));
    }
}

归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用。

归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为 2 - 路归并。

和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn) 的时间复杂度。代价是需要额外的内存空间。

算法步骤

归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下:

  1. 如果输入内只有一个元素,则直接返回,否则将长度为 n 的输入序列分成两个长度为 n/2 的子序列;
  2. 分别对这两个子序列进行归并排序,使子序列变为有序状态;
  3. 设定两个指针,分别指向两个已经排序子序列的起始位置;
  4. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置;
  5. 重复步骤 3 ~4 直到某一指针达到序列尾;
  6. 将另一序列剩下的所有元素直接复制到合并序列尾

图解算法

代码实现

public class MergeSortMain {
    public static void main(String[] args) {
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28, 17};
        arr = mergeSort(arr);
        System.out.println("最终结果:" + Arrays.toString(arr));
    }

    /**
     * 归并排序
     *
     * @param arr
     * @return arr
     */
    public static int[] mergeSort(int[] arr) {
        if (arr.length <= 1) {
            return arr;
        }
        int middle = arr.length / 2;
        int[] arr_1 = Arrays.copyOfRange(arr, 0, middle);
        int[] arr_2 = Arrays.copyOfRange(arr, middle, arr.length);
        return merge(mergeSort(arr_1), mergeSort(arr_2));
    }

    public static int[] merge(int[] arr_1, int[] arr_2) {
        int[] sorted_arr = new int[arr_1.length + arr_2.length];
        int idx = 0, idx_1 = 0, idx_2 = 0;
        while (idx_1 < arr_1.length && idx_2 < arr_2.length) {
            if (arr_1[idx_1] < arr_2[idx_2]) {
                sorted_arr[idx] = arr_1[idx_1];
                idx_1 += 1;
            } else {
                sorted_arr[idx] = arr_2[idx_2];
                idx_2 += 1;
            }
            idx += 1;
        }
        if (idx_1 < arr_1.length) {
            while (idx_1 < arr_1.length) {
                sorted_arr[idx] = arr_1[idx_1];
                idx_1 += 1;
                idx += 1;
            }
        } else {
            while (idx_2 < arr_2.length) {
                sorted_arr[idx] = arr_2[idx_2];
                idx_2 += 1;
                idx += 1;
            }
        }
        return sorted_arr;
    }
}

算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
  • 空间复杂度 :O(n)

快速排序

快速排序用到了分治思想,同样的还有归并排序。乍看起来快速排序和归并排序非常相似,都是将问题变小,先排序子串,最后合并。不同的是快速排序在划分子问题的时候经过多一步处理,将划分的两组数据划分为一大一小,这样在最后合并的时候就不必像归并排序那样再进行比较。但也正因为如此,划分的不定性使得快速排序的时间复杂度并不稳定。

快速排序的基本思想:通过一趟排序将待排序列分隔成独立的两部分,其中一部分记录的元素均比另一部分的元素小,则可分别对这两部分子序列继续进行排序,以达到整个序列有序。

算法步骤

快速排序使用分治法(Divide and conquer)策略来把一个序列分为较小和较大的 2 个子序列,然后递回地排序两个子序列。具体算法描述如下:

  1. 从序列中随机挑出一个元素,做为 “基准”(pivot);
  2. 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个操作结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行快速排序。

图解算法

代码实现

public class QuickSortMain {

    public static void main(String[] args) {
        int[] arr = new int[]{20, 10, 11, 8, 13, 5, 9, 28, 17};
        quickSort(arr, 0, arr.length - 1);
        System.out.println("最终结果:" + Arrays.toString(arr));
    }

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

    public static int partition(int[] array, int low, int high) {
        int pivot = array[high];
        int pointer = low;
        for (int i = low; i < high; i++) {
            if (array[i] <= pivot) {
                int temp = array[i];
                array[i] = array[pointer];
                array[pointer] = temp;
                pointer++;
            }
            System.out.println(Arrays.toString(array));
        }
        int temp = array[pointer];
        array[pointer] = array[high];
        array[high] = temp;
        return pointer;
    }
}

 

搜索算法

 

图算法

 

posted @ 2023-04-09 20:26  残城碎梦  阅读(21)  评论(0编辑  收藏  举报