数据结构——排序算法及Java代码

这一章讲一下排序的内容,还是先看一下分类。



	基本概念-----|- 稳定性
				|- 衡量标准:时间、空间复杂度

								|- 直接插入排序 
				|- 插入排序------|- 折半插入排序
				|				|- 希尔排序
				|
				|- 交换排序------|- 冒泡排序
	内部排序----|				|- 快速排序
				|
				|- 选择排序------|- 简单选择排序
				|				|- 堆排序 
				|- 归并排序
				|- 基数排序

	外部排序-----|- 多路归并排序

一、排序的基本概念

  • 在排序的过程中,根据数据元素是否完全在内存中,可将排序算法分为两类:内部排序和外部排序。
  • 一般情况下,内部排序在算法执行过程中都要进行两种操作:比较和移动。(并不是所有排序算法都是基于比较的,基数排序就不是基于比较的。)
  • 内部排序算法的性能取决于时间复杂度和空间复杂度,时间复杂度一般是由比较和移动次数来决定的。

二、插入排序

1. 直接插入排序

(1)算法思想
为了实现对 L[1…n] 的排序,可以将 L(2)…L(n) 依次插入到前面已经排好序的子序列中。初始假定 L[1] 是一个已排好序的子序列,
① 查找出 L(i) 在 L[1…i-1] 中插入的位置 k ;
② 将 L[k…i-1] 中所有元素全部后移一个位置;
③ 将 L(i) 复制到 L(k);
上述操作执行 n-1 次就能得到一个有序的表。

package SortPackage;

public class InsertSort {

    public static int[] InsertSortFun(int[] nums){

        int i,j,k;
        int index = -1;     //要插入的位置
        int temp = -1;      //要交换的元素
        for (i = 0; i < nums.length; i++){
            int flag = 0;   //记录是否需要发生交换
            for (j = i+1; j < nums.length; j++){
                if (nums[i] > nums[j]){
                    flag = 1;   //需要交换
                    index = i;
                    temp = nums[j];
                    break;
                }
            }
            //将 从i+1开始到j 的元素后移一个位置
            if (flag == 1){
                for (k = j; k > i; k--){
                    nums[k] = nums[k-1];
                }
            }

            //将t插到空出的位置
            nums[index] = temp;
        }

        return nums;
    }

    public static void main(String[] args) {
        int[] nums = {2,1,5,3,1,6,9,3};
        int[] sortNums = InsertSortFun(nums);
        for (int num : sortNums){
            System.out.println(num);
        }
    }

}

在这里插入图片描述

(2)算法性能分析:

  • 空间复杂度:O(1);
  • 时间复杂度:O(n^2);
  • 稳定性:稳定;
  • 适用性:适用于顺序存储和链式存储的线性表;

2. 折半插入排序

(1)算法思想
直接插入算法是边比较边移动元素;折半插入是将比较和移动分离出来,即先折半查找出元素待插入的位置,然后再统一的移动待插入位置之后的所有元素。

package SortPackage;

public class HalfInsertSort {

    public static int[] HalfInsertSortFun(int[] nums){

        int low;
        int high;
        int middle;
        for (int i = 1; i < nums.length; i++){
            low = 0;
            high = i-1;
            int t = nums[i];
            //最小值在low
            while (low <= high){
                middle = (low + high)/2;
                if (t < nums[middle]){
                    high = middle-1;
                }else {
                    low = middle+1;
                }
            }
            //有序表中插入位置后的元素依次后移
            for (int j = i; j > low; j--){
                nums[j] = nums[j-1];
            }
            //将t插到空出的位置
            nums[low] = t;
        }

        return nums;
    }

    public static void main(String[] args) {
        int[] nums = {33,12,25,46,33,68,19,80};
        int[] sortNums = HalfInsertSortFun(nums);
        for (int num : sortNums){
            System.out.println(num);
        }
    }

}

(2)性能分析:

  • 空间复杂度:O(1);
  • 时间复杂度:O(n^2);虽然和直接插入相同,但对于数据量较小的排序表,性能较好;
  • 稳定性:稳定;
  • 适用性:适用有序的表;

3. 希尔排序

又称:缩小增量排序。
(1)算法思想
先将待排序表分成若干个子表 L[i, i+d, i+2d,…,i+kd] ,分别进行直接插入排序,当整个表中元素已基本有序时,再对全体记录进行一次插入排序。

void ShellSort(ElemType A[], int n){
// 代码后续补充
}

(2)性能分析

  • 空间复杂度:O(1);
  • 时间复杂度:O(n^2);
  • 稳定性:不稳定;
  • 适用性:适用于线性表为顺序存储;

三、交换排序

1. 冒泡排序

(1)算法思想
假设待排序表长为 n ,从前往后两两比较相邻元素的值,若为逆序,则交换它们,直到序列比较完。我们称之为一趟冒泡,结果将最小的元素交换到序列的第一个位置。
下一趟冒泡时,最小元素不再参与,待排序元素减少一个。
每趟冒泡的的结果把序列中最小元素放到了序列的最终位置。

package SortPackage;

public class BubbleSort {

    public static int[] BubbleSortFun(int[] nums){

        for (int i = 0; i < nums.length-1; i++){
            for (int j = 0; j < nums.length-1; j++){
                if (nums[j] > nums[j+1]){
                    int t = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = t;
                }
            }
        }
        return nums;
    }

    public static void main(String[] args) {
        int[] nums = {33,68,46,33,7,25,80,19,12};
        int[] sortNums = BubbleSortFun(nums);
        for (int num : sortNums){
            System.out.println(num);
        }
    }

}

在这里插入图片描述

(2)性能分析

  • 空间复杂度:O(1);
  • 时间复杂度:O(n^2);
  • 稳定性:稳定;

2. 快速排序

(1)算法思想

  • 是对冒泡排序算法的一种改进,基本思想是基于分治的。
  • 在表中选取一个基准元素pivot,通过一趟排序将待排序表划分为独立的两部分 L[1…k-1] 和 L[k+1…n],使得 L[1…k-1] 中所有元素小于pivot,L[k+1…n] 中所有元素大于或等于pivot,pivot放在最终位置 k 上。
  • 快速排序的关键在于划分操作,算法性能也主要取决于划分操作的好坏。
  • 目前常用的方法是:将当前表中的第一个元素作为枢纽值对表进行划分,比枢纽值大的元素向右移动,比枢纽值小的元素向左移动,使得一趟Partition() 操作之后,表中元素被枢纽值一分为二。
package SortPackage;

public class QuickSort {
    public static void quickSort(int[] arr,int low,int high){
        int i,j,temp,t;
        if(low > high){
            return;
        }
        i = low;
        j = high;
        //temp就是基准位
        temp = arr[low];

        while (i < j) {
            //先看右边,依次往左递减
            while (temp<=arr[j] && i<j) {
                j--;
            }
            //再看左边,依次往右递增
            while (temp>=arr[i] && i<j) {
                i++;
            }
            //如果满足条件则交换
            if (i < j) {
                t = arr[j];
                arr[j] = arr[i];
                arr[i] = t;
            }
        }
        //最后将 基准元素 与 i=j时位置的数字 交换
        arr[low] = arr[j];  //此时i,j相等,这两行用i或j都可以
        arr[j] = temp;
        //递归调用左半数组
        quickSort(arr, low, j-1);
        //递归调用右半数组
        quickSort(arr, j+1, high);
    }


    public static void main(String[] args){
        int[] arr = {10,7,2,4,3,2,1,8,9};
        quickSort(arr, 0, arr.length-1);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

在这里插入图片描述

(2)性能分析

  • 空间复杂度:O(log2 n);
  • 时间复杂度:O(nlog2 n);
  • 稳定性:不稳定;

四、选择排序

选择排序基本思想:每一趟在后面 n-i+1 个待排序元素中取关键字最小的元素,直到第 n-1 趟做完,待排序元素只剩下一个,就不用了再选了。

1. 简单选择排序

(1)算法思想:
第 i 趟排序即从 L[1…n] 中选择关键字最小的元素与 L(i) 交换,每一趟排序可以确定一个元素的最终位置,这样经过 n-1 趟排序就可以使得整个表有序。

package SortPackage;

public class SelectSort {

    public static int[] SelectSortFun(int[] nums){

        for (int i = 0; i < nums.length-1; i++){
            int minIndex = i;   //将i位置 当作 目前最小值元素的位置minIndex
            for (int j = i+1; j < nums.length; j++){    //前i个已经有序,从i+1开始遍历
                if (nums[minIndex] > nums[j]){  //如果minIndex不是较小的
                    minIndex = j;   //则将 较小的元素位置j 赋值给 minIndex
                }
            }
            if (minIndex != i){     //遍历一趟之后,如果最开始的i 和 最终的minIndex 不同
                int t = nums[i];    //则交换这两个位置的元素
                nums[i] = nums[minIndex];
                nums[minIndex] = t;
            }
        }
        return nums;
    }

    public static void main(String[] args) {
        int[] nums = {98,68,46,33,25,80,19,12,33};
        int[] sortNums = SelectSortFun(nums);
        for (int num : sortNums){
            System.out.println(num);
        }
    }
}

在这里插入图片描述

(2)性能分析:

  • 空间复杂度:O(1);
  • 时间复杂度:O(n^2);
  • 稳定性:不稳定;

2. 堆排序

(1)堆的概念:

  • 堆排序是一种树形选择排序方法,特点是:在排序过程中,将 L[1…n] 看成是一棵完全二叉树的顺序存储结构。
  • 堆的定义:
    • 小顶堆:L(i) <= L(2i) 且 L(i) <= L(2i+1);
    • 大顶堆:L(i) >= L(2i) 且 L(i) >= L(2i+1);
  • 堆的应用:堆经常被用来实现优先级队列,优先级队列在操作系统的作业调度和其他领域有着广泛的应用。
    (2)算法思想
  • 堆排序的关键是构造初始堆,对初始序列建堆,就是一个反复筛选的过程。
    在这里插入图片描述
//建立大顶堆的方法
void BuildMaxHeap(ElemType A[], int len){

}
void AdjustDown(ElemType A[], int len){

}
  • 向下调整的时间与树高度有关,为O(h)。其时间复杂度为O(n)。
  • 首先将存放在 L[1…n] 中的 n 个元素建成初始堆。
  • 输出堆顶元素后,通常将堆底元素送入堆顶,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩下一个元素为止。
    在这里插入图片描述
//堆排序的算法
void HeapSort(ElemType A[], int len){

}
  • 堆也支持插入和删除操作。
    (1)删除:删除堆顶元素时,先将堆的最后一个元素与堆顶元素交换,由于此时堆的性质被破坏,需要对此时的根节点进行向下调整操作。
    (2)插入:先将新结点放在放在堆的末端,再对新结点执行向上调整操作。
//向上调整堆的算法
void AdjustUp(ElemType A[], int k){

}

(3)性能分析

  • 空间复杂度:O(1);
  • 时间复杂度:O(nlog2 n);
  • 稳定性:不稳定;

五、归并排序和基数排序

1. 归并排序

(1)算法思想

  • 假定待排序表中含有 n 个记录,则可以看成是 n 个有序的子表,每个子表长度为1,然后两两归并,如此重复,直到合并成一个表为止。这种排序算法称为 2-路归并排序。
    在这里插入图片描述

  • 递归形式的 2-路归并排序算法是基于分治的,其过程如下:
    (1)分解:将含有 n 个元素的待排序表分成含 n/2 个元素的子表,采用 2-路归并排序对两个子表递归的进行排序。
    (2)合并:合并两个已排序的子表得到排序结果。

package SortPackage;

public class MergeSort {

    //对上下限分别为 left 和 right 的记录惊醒二路归并排序
    public static void MergeSort(int nums[], int copy[], int left, int right){
        int middle;
        if (left < right){
            middle = (left+right) / 2;  //找中间位置进行划分
            MergeSort(nums, copy, left, middle);    //对左半部分进行递归归并排序
            MergeSort(nums, copy, middle+1, right); //对右半部分进行递归归并排序
            Merge(nums, copy, left, right, middle); //进行归并
        }
    }

    //二路归并过程
    public static void Merge(int nums[], int copy[], int left, int right, int middle){
        int i, p1, p2;
        for (i = left; i <= right; i++){    //将nums中的元素暂时复制到copy
            copy[i] = nums[i];
        }
        p1 = left;
        p2 = middle+1;
        i = left;
        //将左右两部分中,较小的元素依次放入nums
        while (p1<=middle && p2<=right){    //用 p1,p2 分别遍历数组的前后两部分
            if (copy[p1] <= copy[p2]){  //如果p1指向的元素小,则将p1指向的元素放到nums[i]
                nums[i] = copy[p1];
                p1++;
            }
            else{   //如果p2指向的元素小,则将p2指向的元素放到nums[i]
                nums[i] = copy[p2];
                p2++;
            }
            i++;
        }
        //将左右两部分中剩余的元素,直接复制到nums
        while (p1 <= middle){
            nums[i] = copy[p1];
            i++;
            p1++;
        }
        while (p2 <= right){
            nums[i] = copy[p2];
            i++;
            p2++;
        }

    }

    public static void main(String[] args) {
        int[] nums = {2,7,5,3,1,6,9,3};
        int[] copy = new int[nums.length];
        int left = 0;
        int right = nums.length-1;
        MergeSort(nums, copy, left, right);
        for (int i = 0; i < nums.length; i++) {
            System.out.println(nums[i]);
        }
    }
}

(2)性能分析

  • 空间复杂度:O(n);
  • 时间复杂度:O(nlog2 n);( 每一趟归并的时间复杂度为O(n) )
  • 稳定性:稳定;

2. 基数排序

(1)算法思想:

  • 基数排序不是基于比较进行的,而是采用多关键字排序思想(即基于关键字各位的大小进行排序的),借助“分配”和“收集”两种操作对单逻辑关键字进行排序。
  • 基数排序又分为最高位优先(MSD)和最低位优先(LSD)排序。
    在这里插入图片描述
package SortPackage;

public class RadixSort {

    public static void radixSort(int nums[]) {
        int divisor = 1; // 定义每一轮的除数,1,10,100...
        int[][] bucket = new int[10][nums.length]; // 定义了10个桶,以防每一位都一样全部放入一个桶中
        int[] count = new int[10]; // 统计每个桶中实际存放的元素个数
        int digit; // 获取元素中对应位上的数字,即装入那个桶

        for (int i = 1; i <= 3; i++) { // 经过4次装通操作,排序完成
            for (int temp: nums) { // 计算入桶
                digit = (temp / divisor) % 10;
                bucket[digit][count[digit]++] = temp;
            }
            int index = 0; // 被排序数组的下标
            for (int j = 0; j < 10; j++) { // 从0到9号桶按照顺序取出
                if (count[j] == 0) // 如果这个桶中没有元素放入,那么跳过
                    continue;
                for (int k = 0; k < count[j]; k++) {
                    nums[index++] = bucket[j][k];
                }
                count[j] = 0; // 桶中的元素已经全部取出,计数器归零
            }
            divisor *= 10;  // 除数每次乘10,按此方式增长:1,10,100...
        }
    }

    public static void main(String[] args) {
        int[] nums = {261,123,534,322,326,567,125,666};
        radixSort(nums);
        for (int i = 1; i < nums.length; i++){
            System.out.println(nums[i]);
        }
    }

}

(2)性能分析:

  • 空间复杂度:O(r );一趟排序需要借助的辅助存储空间为 r (r个队列)。
  • 时间复杂度:O(d(n+r));一趟分配需要O(n),一趟收集需要O(r )。
  • 稳定性:稳定;

六、各种内部排序算法的比较及应用

1. 内部排序算法的比较

对于任意一种基于比较的排序,最少比较次数为(n 为关键字个数):
在这里插入图片描述

算法算法种类时间(最好)时间(平均)时间(最坏)空间复杂度稳定性备注
插入直接插入排序O(n)O(n^2)O(n^2)O(1)稳定最少比较(n-1)次;最多比较n(n-1)/2次;
交换冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
选择简单选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定比较次数O(n^2);移动次数O(n);比较次数与序列初始状态无关
插入希尔排序O(1)不稳定
交换快速排序O(nlog2 n)O(nlog2 n)O(n^2)O(log2 n)不稳定最大递归深度为n,最小递归深度(log2 n);最坏空间复杂度O(n)
选择堆排序O(nlog2 n)O(nlog2 n)O(nlog2 n)O(1)不稳定构建堆时间复杂度O(n);堆排序时间复杂度最坏O(nlog2 n);插入删除时间复杂度O(log2 n);查找效率较低
归并2-路归并排序O(nlog2 n)O(nlog2 n)O(nlog2 n)O(n)稳定比较次数与序列初始状态无关,归并趟数O(log2 n)
基数基数排序O(d(n+r))O(d(n+r))O(d(n+r))O(r )稳定元素移动次数与序列初始状态无关
  • 在每趟排序过程中,都会有一个元素被放到最终位置上,这样的排序方式有:堆排序、冒泡排序、快速排序。
  • 直接插入排序、希尔排序,在最后一趟排序之前,所有元素都可能不在最终位置上。

2. 内部排序算法的应用

(1)选取排序方法需要考虑的因素
① 待排序的元素数目 n ;
② 元素本身信息量大小;
③ 关键字的结构及其分布情况;
④ 稳定性的要求;
⑤ 语言工具条件,存储结构及辅助空间大小。
(2)小结
① 若 n 较小,则可以采用直接插入排序或简单选择排序;
② 若文件初始状态关键字基本有序,则用直接插入或冒泡;
③ 若 n 较大,则应采用快速排序、堆排序、归并排序;
④ 当记录本身信息量较大时,为了避免耗费大量的时间移动记录,可用链表作为存储结构。

七、外部排序

1. 外部排序的基本概念

  • 对大文件进行排序,无法将整个文件拷贝到内存中进行排序。
  • 因此要将待排序记录存储在外存上,排序时再把数据一部分一部分的调入内存中进行排序。
  • 在排序过程中,需要多次进行内存和外存之间的交换,排序后的结果仍然存在原有文件中。

2. 外部排序的方法

3. 多路平衡归并与败者树

4. 置换-选择排序

5. 最佳归并树

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值