数据结构与算法 排序算法

目录

时间频度

        基本介绍

        举例说明-基本案例

忽略常数项

忽略低次项

 忽略系数

 

时间复杂度

常见的时间复杂度实例

排序算法

冒泡排序

选择排序

 插入排序

希尔排序

简单插入排序存在的问题

希尔排序

希尔排序【交换式】

希尔排序【移动法】

   快速排序


时间频度

        基本介绍

时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间
就多。 一个算法中的语句执行次数称为语句频度或时间频度 。记为 T(n) [ 举例说明 ]

        举例说明-基本案例

比如计算 1-100 所有数字之和 , 我们设计两种算法:
        

忽略常数项

        

忽略低次项

 忽略系数

时间复杂度

1) 一般情况下, 算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数 ,用 T(n) 表示,若有某个辅助函数 f(n) ,使得当 n 趋近于无穷大时, T(n) / f(n) 的极限值为不等于零的常数,则称 f(n) T(n) 的同数量级函数。
记作 T(n)= ( f(n) ) ,称O ( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度。
2) T(n) 不同,但时间复杂度可能相同。 如: T(n)=n ² +7n+6 T(n)=3n ² +2n+2 它们的 T(n) 不同,但时间复杂
度相同,都为 O(n ² )
3) 计算时间复杂度的方法:
  •  用常数 1 代替运行时间中的所有加法常数 T(n)=n²+7n+6 => T(n)=n²+7n+1
  • 修改后的运行次数函数中,只保留最高阶项 T(n)=n²+7n+1 => T(n) = n²
  • 去除最高阶项的系数 T(n) = n² => T(n) = n² => O(n²)

常见的时间复杂度实例

        常见的时间复杂度实例

排序算法

        冒泡排序

/**
     * 冒泡排序
     * 时间复杂度O(n*n)
     * 结合图解,5个元素只需要排4趟,所以第一个for循环是length-1次
     * 每趟里最多比较4次,且每到下一趟都会少比较一次。所以第二个for循环是 length-1-i次
     * @param nums
     * @return
     */
    public static int[] bubbleSort(int[] nums){
        System.out.println("没有排序前:"+Arrays.toString(nums));
        for (int i = 0; i < nums.length - 1; i++) { //这里控制排多少躺
            for (int j = 0; j < nums.length - i - 1; j++) { //这里控制一趟排多少次
                if(nums[j] > nums[j+1]){
                    swap(nums,j,j+1);
                }
            }
            System.out.println("第"+(i+1)+"次排序:"+Arrays.toString(nums));
        }
        return nums;
    }
    public static void swap(int[] nums,int i,int j){
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

可以稍稍优化一下,如果有某趟排序里没有发生过交换,那就直接结束排序。

/**
     * 冒泡排序
     * 时间复杂度O(n*n)
     * 结合图解,5个元素只需要排4趟,所以第一个for循环是length-1次
     * 每趟里最多比较4次,且每到下一趟都会少比较一次。所以第二个for循环是 length-1-i次
     * @param nums
     * @return
     */
    public static int[] bubbleSort(int[] nums){
        System.out.println("没有排序前:"+Arrays.toString(nums));
        boolean flag = false; //表示变量,表示是否发生过交换
        for (int i = 0; i < nums.length - 1; i++) { //这里控制排多少躺
            for (int j = 0; j < nums.length - i - 1; j++) { //这里控制一趟排多少次
                if(nums[j] > nums[j+1]){
                    flag = true; //发生过交换,排序继续进行
                    swap(nums,j,j+1);
                }
            }
            if (!flag){ //取反,如果flag没有发生过交换,那就说明已经排序完成,直接结束排序
                break;
            }else {
                flag = false; //重置flag
            }
            System.out.println("第"+(i+1)+"次排序:"+Arrays.toString(nums));
        }
        return nums;
    }

 两次排序就可以完成,无需排序4次。

选择排序

选择排序( select sorting )也是一种简单的排序方法。
它的基本思想是:第一次从 arr[0]~arr[n1] 中选取最小值, arr[0] 交换,第二次从 arr[1]~arr[n-1] 中选取最小值,与 arr[1] 交换,第三次从 arr[2]~arr[n-1] 中选取最小值,与 arr[2] 交换,…,第 i 次从 arr[i-1]~arr[n-1] 中选取最小值,与 arr[i-1] 交换,… , n-1 次从 arr[n-2]~arr[n-1] 中选取最小值, arr[n-2] 交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。
/**
     * 选择排序 O(n*n)
     * 每次只比较length-1次
     * 在每趟排序开始,先假设数组第i个数是最小数(第一轮就是下标为0的,第二轮就是下标为1的)
     * 然后依次和后面的数进行比较,如果发现num[min]比后面的数大,那就将比较小的数的下标记录到min
     * 在一趟比较完后,把i和min进行交换,如此执行到length-1次为止
     * @param nums
     * @return
     */
    public static int[] selectSort(int[] nums){
        System.out.println("排序前:"+Arrays.toString(nums));
        for (int i = 0; i < nums.length - 1; i++) {
            int min = i;
            for (int j = i; j < nums.length; j++) {
                if (nums[min] > nums[j]){
                    min = j;
                }
            }
            BubbleSort.swap(nums,min,i);
            System.out.println("第"+(i+1)+"次排序:"+Arrays.toString(nums));
        }
        return nums;
    }

 

 插入排序

       插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 [1]  。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。

        

将一个数插入一个已经排好序的数据中。

  1. 第一次循环时,从第2个数开始处理。我们将第1个数作为已经排好序的数据:当第2个数 > 第1个数时,将第2个数放在第1个数后面一个位置;否则,将第2个数放在第1个数前面。此时,前两个数形成了一个有序的数据。
  2. 第二次循环时,我们处理第3个数。此时,前两个数形成了一个有序的数据:首先比较第3个数和第2个数,当第3个数 > 第2个数时,将第3个数放在第2个数后面一个位置并结束此次循环;否则,再和第1个数比较。如果第3个数 > 第1个数,则将第3个数插入第1个数和第2个数中间;否则,第3个数 < 第1个数,则将第3个数放在第1个数前面。此时,前三个数形成了一个有序的数据。
  3. 后续的数据同理处理,直至结束。

作者:程序员囧辉
链接:https://zhuanlan.zhihu.com/p/35328552
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

/**
     * 插入排序 O(n*n)
     *  将第一个元素看作是一个有序数组,然后从第二个元素开始,将元素依次插入到有序数组里
     *   假设第二个元素34插入到有序数组内 34 < 101,34就会向后一个元素进行比较。发现没有元素或没有比自己小的了,就交换位置。
     * @param arr
     * @return
     */
    public static int[] insertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) { //设arr[0]是一个有序数组,所以从arr[1]开始进行排序操作
            if (arr[i] < arr[i - 1]){ //假设当前定位的数,比有序数组里最大的数要小(34 < 101,如果是334的话那就不需要排序了,占住当前的位置就行)
                int temp = arr[i];    //为了插入进去,首先保存下要插入的数
                int j = i;            //记录下i的值,方便去有序数组里遍历查找插入位置
                while (j > 0 && temp < arr[j - 1]){
                    //j必须大于0,否则就会数组越界。被插入的数如果小于当前遍历到的数,那就继续循环下去
                    // 直到找到最左一个位置或者一个比自己大的数
                    arr[j] = arr[j-1]; //每找到一个比temp大的数,那就这个数的位置往右挪一格
                    j--; //去遍历下一个数
                }
                arr[j] = temp;//当循环结束的时候,把temp插入进去
            }
        }
        return arr;
    }

希尔排序

简单插入排序存在的问题

我们看简单的插入排序可能存在的问题 .
数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1 ( 最小 ), 这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
结论 : 需要插入的数是较小的数时 后移的次数明显增多 ,对 效率 有影响 .

希尔排序

        希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 ,整个文件恰被分成一组,算法便终止

希尔排序【交换式】

三轮拆分

public static void main(String[] args) {
        int[] nums = new int[]{8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
        shellSort(nums);
    }

    public static void shellSort(int[] nums) {
        //shell第一轮
        //因为第一轮是将10个数组分成5组
        System.out.println("-----------第一轮--------------");
        for (int i = 5; i < nums.length; i++) {
            //遍历各组中所有的元素(共5组,每组两个元素),每次步长都是5
            for (int j = i - 5; j >= 0; j -= 5) {
                //如果当前元素大于加上步长后的那个元素,说明要进行交换
                //这里看起来的感觉就是 8和3进行比较 因为j=0 j+5=5 即arr[0]与arr[5]
                if (nums[j] > nums[j + 5]) {
                    BubbleSort.swap(nums, j, j + 5);//交换
                }
            }
        }
        System.out.println(Arrays.toString(nums));
        System.out.println("-----------第二轮--------------");
        //shell第二轮
        //因为第一轮是将10个数组分成5组
        for (int i = 2; i < nums.length; i++) {
            //遍历各组中所有的元素(共5组,每组两个元素),每次步长都是5
            for (int j = i - 2; j >= 0; j -= 2) {
                //如果当前元素大于加上步长后的那个元素,说明要进行交换
                //这里看起来的感觉就是 8和3进行比较 因为j=0 j+5=5 即arr[0]与arr[5]
                if (nums[j] > nums[j + 2]) {
                    BubbleSort.swap(nums, j, j + 2);//交换
                }
            }
        }
        System.out.println(Arrays.toString(nums));
        System.out.println("-----------第三轮--------------");
        //shell第二轮
        //因为第一轮是将10个数组分成5组
        for (int i = 1; i < nums.length; i++) {
            //遍历各组中所有的元素(共5组,每组两个元素),每次步长都是5
            for (int j = i - 1; j >= 0; j -= 1) {
                //如果当前元素大于加上步长后的那个元素,说明要进行交换
                //这里看起来的感觉就是 8和3进行比较 因为j=0 j+5=5 即arr[0]与arr[5]
                if (nums[j] > nums[j + 1]) {
                    BubbleSort.swap(nums, j, j + 1);//交换
                }
            }
        }
        System.out.println(Arrays.toString(nums));
    }

将三轮汇总后


    /**
     * 希尔排序
     * @param nums
     */
    public static int[] shellSort(int[] nums) {
        int count = 0;
        for (int gap = nums.length / 2 ; gap > 0; gap/=2){
            for (int i = gap; i < nums.length; i++) {
                //遍历各组中所有的元素(共gap组,每组根据分组来决定元素数量),每次步长都是gap
                for (int j = i - gap; j >= 0; j -= gap) {
                    //如果当前元素大于加上步长后的那个元素,说明要进行交换
                    //这里看起来的感觉就是 8和3进行比较 因为j=0 j+5=5 即arr[0]与arr[5]
                    if (nums[j] > nums[j + gap]) {
                        BubbleSort.swap(nums, j, j + gap);//交换
                    }
                }
            }
            System.out.printf("希尔排序第%d轮后结果:",++count);
            System.out.println(Arrays.toString(nums));
        }
        return nums;
    }

希尔排序【移动法】

就是把gap里的循环替换,换成插入排序

/**
     * 希尔排序【移动法】
     * @param nums
     */
    public static int[] shellSortInsert(int[] nums) {
        int count = 0;
        for (int gap = nums.length / 2 ; gap > 0; gap/=2){
            //从第gap个元素,逐个对其所在的组进行直接插入排序
            for (int i = gap; i < nums.length; i++) {
                int j = i;
                int temp = nums[j];
                if(nums[j] < nums[j - gap]){
                    while (j - gap >= 0 && temp < nums[j - gap]){
                        //移动
                        nums[j] = nums[j - 1];
                        j--;
                    }
                    //当退出while循环后就说明找到位置了
                    nums[j] = temp;
                }
            }
            System.out.printf("希尔排序第%d轮后结果:",++count);
            System.out.println(Arrays.toString(nums));
        }
        return nums;
    }

   快速排序

核心思想:

1.在待排序的元素任取一个元素作为基准(通常选第一个元素,称为基准元素)

2.将待排序的元素进行分块,比基准元素大的元素移动到基准元素的右侧,比基准元素小的移动到作左侧,从而一趟排序过程,就可以锁定基准元素的最终位置

3.对左右两个分块重复以上步骤直到所有元素都是有序的(递归过程)

一个很好的快排图解

public static void main(String[] args) {
        int[] nums = new int[]{-9,78,0,23,12,70, -1,900, 4561};
        quickSort(nums,0,nums.length-1);
        System.out.println(Arrays.toString(nums));
    }
    public static void quickSort(int[] arr,int left,int right){
        int l = left;  //左下标
        int r = right;  //右下标
        int pivot = arr[(left+right)/2]; //确定中轴值 以数组中间为基准

        //while循环的目的是让比基准值(pivot)小的放到左边
        //比基准值(pivot)大的放到右边
        while (l < r){
            //在pivot的左边一直找,直到找到一个比pivot大或相等的
            while (arr[l] < pivot){
                l+=1;
            }
            //在pivot的右边一直找,直到找到一个比pivot小或相等的
            while (arr[r] > pivot){
                r-=1;
            }

            //l >= r 说明pivot的左右两边的值 左边全是小于pivot的值,右边全是大于pivot的值
            if (l >= r){
                break;
            }
            //找到以后开始交换
            BubbleSort.swap(arr,l,r);

            //如果交换完后发现这个arr[l] == pivot值,则r向左移动一步
            if (arr[l] == pivot){
                r-=1;
            }else if(arr[r] == pivot){ //反之则l向右移动一步
                l+=1;
            }
        }
        if(l == r){
            l+=1;
            r-=1;
        }
        if (left < r){
            quickSort(arr,left,r);
        }
        if (right > l){
            quickSort(arr,l,right);
        }
       

    }

归并排序

归并排序( MERGE-SORT )是利用归并的思想实现的排序方法,该算法采用经典的 分治( divide-and-conquer )策略 (分治法将问题分 (divide) 成一些 小的问题然后递归求解 ,而治 (conquer) 的阶段则将分的阶段得到的各答案 " " 在一起,即分而治之 )

基本思想

归并排序的主要思想是分治法。主要过程是:

  1. 将n个元素从中间切开,分成两部分。(左边可能比右边多1个数)
  2. 将步骤1分成的两部分,再分别进行递归分解。直到所有部分的元素个数都为1。
  3. 从最底层开始逐步合并两个排好序的数列。

合并相邻有序子序列:

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将 [4,5,7,8] [1,2,3,6] 两个已经有序的子序列,合并为最终序列 [1,2,3,4,5,6,7,8] ,来看下实现步骤

 

public class MergeSort {
    public static void main(String[] args) {
        int[] arr = new int[] {8, 4, 5, 7, 1, 3, 6, 2};
        mergeSort(arr,0,arr.length-1,new int[arr.length]);
        System.out.println(Arrays.toString(arr));
    }

    // 归并
    private static void mergeSort(int array[], int first, int last, int temp[]) {
        if (first < last) {
            int mid = (first + last) / 2;
            mergeSort(array, first, mid, temp); // 递归归并左边元素
            mergeSort(array, mid + 1, last, temp); // 递归归并右边元素
            merge(array, first, mid, last, temp); // 再将二个有序数列合并
        }
    }

    /**
     * @param arr   待排序的数组
     * @param left  左边有序序列的初始索引
     * @param right 右边有序序列的最终索引
     * @param mid   中间索引
     * @param temp  临时数组
     */
    public static void merge(int[] arr, int left,int mid, int right, int[] temp) {
        //初始化i,表示左边有序序列的初始索引
        //初始化j,表示右边有序序列的初始化索引
        int i = left,j = mid + 1;
        int m = mid,n = right;
        int t = 0; //用来将数放到temp数组里的索引。指向temp数组的当前索引。
        //(一)
        //先把左右两边的数据按规则填充到temp数组
        //直到左右两边的有序序列有一边处理完毕为止
        while (i <= m && j <= n){ //两边的有序序列还未比较完,继续比较
            if (arr[i] <= arr[j]){
                //将两边数组的数进行比较,比较小的一方先进入temp数组
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        //(二)
        //把有剩余数据的一方依次填充到temp去
        //如果左边有剩下的
        while (i <= m){
            temp[t++] = arr[i++];
        }
        //如果右边有剩下的
        while (j <= n){
            temp[t++] = arr[j++];
        }
        //(三)
        //将temp数组的元素拷贝到arr
        for (i = 0; i < t; i++) {// 将排好序的数填回到array数组的对应位置
            arr[left + i] = temp[i];
        }
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值