【算法】排序

一、时间复杂度(表示数据量和算法流程的关系)

一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
在表达式中,只要高阶项和它的系数,不要低阶项,剩下的部分如果为f(N),则时间复杂度为O(f(N))。
比如如下计算:

for (int i = 0;i < list.size();i++){
            System.out.print(list.get(i));
        }

对于arraylist而言,内部是线性表实现,get()方法直接获取index上的值,时间复杂度是O(1),遍历
整个集合的时间复杂度是1+1+... = O(N)
而基于链表实现的LinkedList类,只知道链表指针指向的下一个元素,所以需要通过for循环从头或尾开始查找,
当i = 0时,遍历一个数,复杂度为1;
当i = 1时,遍历两个数,复杂度为2;
...
当i = N - 1时,遍历N个数,复杂度为N;
所以,LinkedList整体时间复杂度为1+2+...+N ,0(N^2)

二、空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,指除去原始数据外所需要的额外空间。如果空间复杂度与数据量的大小无关,则空间复杂度大小描述为O(1)。

三、几种基础排序

https://visualgo.net/en/sorting

1、选择排序

把每个位置与其后每一个数进行比较,获取到最小的那个数,然后把它与当前位置交换。
时间复杂度0(N^2)

    public static void selectionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 0; i < arr.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr, i, minIndex);
        }
    }

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

2、冒泡排序

  相邻元素之间对比,谁大谁在后。
  时间复杂度0(N^2)
   public static void bubbleSort(int[] arr) {
       if (arr == null || arr.length < 2) {
           return;
       }
       for (int e = arr.length - 1; e > 0; e--) {
           for (int i = 0; i < e; i++) {
               if (arr[i] > arr[i + 1]) {
                   swap(arr, i, i + 1);
               }
           }
       }
   }

3、插入排序

每次比较前N个数,谁大谁在后。
取最坏情况,时间复杂度0(N^2)

   public static void insertionSort(int[] arr) {
       if (arr == null || arr.length < 2) {
           return;
       }
       for (int i = 1; i < arr.length; i++) {
           for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
               swap(arr, j, j + 1);
           }
       }
   }

** 异或运算:无进位相加。相同为0,不同为1.
如果想交换两个数,可使用以下方式:

a = a ^ b
b = a ^ b
a = a ^ b

**

4、归并排序

是典型的分治算法,分治法是将一个问题分解成规模更小、结构相似的子问题,解决问题A,变成了解决问题A1和A2,解决问题A1变成了解决问题A11和A12。。。,一直到最小单元,当最小单元问题解决后,依次向上返回,问题A得以解决。
这里整体是一个简单的递归,左边排好序,右边排好序,然后让其整体有序。
递归行为:压栈,从最小单元依次向上返回
(图片来自网络)


image.png
    public static void mergeSort(int[] arr, int L, int r) {
        if (L == r) {
            return;
        }
        int mid = L + ((r - L) >> 1);
        mergeSort(arr, L, mid);
        mergeSort(arr, mid + 1, r);
        merge(arr, L, mid, r);
    }

    public static void merge(int[] arr, int L, int m, int r) {
        int[] help = new int[r - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = m + 1;
        while (p1 <= m && p2 <= r) {
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= m) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[L + i] = help[i];
        }
    }

5、堆排序

时间复杂度O(nlogn)
堆结构是用数组实现的完全二叉树结构。
完全二叉树中如果每个子树的最大值都在顶部就是大根堆,如果最小值都在顶部则是小根堆。
完全二叉树:若设二叉树的深度为h,除最后一层外,其它各层 (1~h-1) 的结点数都达到最大个数,最后一层所有的结点都从左到右排列,这就是完全二叉树。


image.png

对于一颗完全二叉树,位置i处的左节点:i2+1,右节点:i2+2,父节点:(i-1)/2. 高度:log2^N

优先队列PriorityQueue就是小根堆,保证每次弹出的都是最小值:

 PriorityQueue<Integer> queue = new PriorityQueue<>();
            queue.add(3);
            queue.add(5);
            queue.add(1);
            while (!queue.isEmpty()){
                System.out.print(queue.poll());
            }
打印结果:----1,3,5----------------

堆排序的思想:
把待排序的数组构造成一个大根堆,然后将首尾元素进行交换,此时末位置是最大值。然后将剩余的n-1个元素重新构造成大根堆,这样就得到n个元素的次小值,重复执行,直到堆的大小为0,就得到一个有序的序列。

public static void heapSort(int[] arr) {
       if (arr == null || arr.length < 2) {
           return;
       }
       //数组变成大根堆  O(n)
       for (int i = 0; i < arr.length; i++) {
           heapInsert(arr, i);
       }

       int size = arr.length;
       //交换最大值和堆中最后一个数,堆的大小变为size-1
       swap(arr, 0, --size);

       //从0位置开始,重新调整成大根堆 O(logN)
       while (size > 0) {
           heapify(arr, 0, size);
           swap(arr, 0, --size);
       }
   }

   //index位置处的值发生了变化,并且是变大,重新调整成大根堆
   public static void heapInsert(int[] arr, int index) {
       while (arr[index] > arr[(index - 1) / 2]) {
           swap(arr, index, (index - 1) /2);
           index = (index - 1)/2 ;
       }
   }

   //index位置处的值发生了变化,并且是变小,重新调整成大根堆
   public static void heapify(int[] arr, int index, int size) {
       int left = index * 2 + 1;
       while (left < size) {//index位置处是否还有子节点
           int largest = left + 1 < size  //如果有右节点
                   && arr[left + 1] > arr[left]  //右节点的值大于左节点
                   ? left + 1 : left;
           //两个孩子和父节点的值比较,最大的值的下表作为largest的值
           largest = arr[largest] > arr[index] ? largest : index;

           if (largest == index) {//如果最大的值的下标已经是父节点的位置,过程停止
               break;
           }
           //选出来的largest的位置一定不是i位置,是i位置的左右两个孩子中较大值的下标
           swap(arr, largest, index);
           index = largest;
           left = index * 2 + 1;
       }
   }

6、快速排序

快速排序是目前应用最广泛的排序算法之一,它是一般场景中大规模数据排序的首选。
partition过程:每次排序的时候设置一个基准点,将小于基准点的数全部放到基准点的左边,将大于基准点的数全部放到基准点的右边。

当前数<p,当前数和<区后一个数做交换,<区右扩,当前位置跳下一个。
当前数=p,当前位置跳下一个。
当前数>p,当前数和>区前一个交换,>区左扩,当前位置不变。

以数组的最后一个数为P,然后再把<区和>区分别进行partition

    public static void quickSort(int[] arr, int L, int r) {
        if (L < r) {
            //改进方法:随机快排,从R-L中随机选一个数,放到最后
//          swap(arr, L + (int) (Math.random() * (r - L + 1)), r);
            int[] p = partition(arr, L, r);
            //小于区
            quickSort(arr, L, p[0] - 1);
            //大于区
            quickSort(arr, p[1] + 1, r);
        }
    }

    //默认以arr[r]为区分
    //返回等于区域(左边界,右边界),所以返回一个长度为2的数组res,res[0] res[1]
    public static int[] partition(int[] arr, int L, int r) {
        int less = L - 1; //小于区右边界
        int more = r; //大于区左边界,包含最后一个数
        while (L < more) {
            if (arr[L] < arr[r]) {//默认以arr[r]做划分,当前数 < 划分值时
                swap(arr, ++less, L++);
            } else if (arr[L] > arr[r]) {
                swap(arr, --more, L);
            } else {
                L++;
            }
        }
        //在大于区域最后一个数和第一个数交换,因为最后一个数属于等于区
        swap(arr, more, r);
        return new int[] { less + 1, more };
    }

时间复杂度:最差的情况O(N^2),比如顺序排列的数,以最后一个数做划分,每次只能确定一个数。平均时间复杂度为O(n×log(n))。

7、桶排序(不基于比较的排序)

计数排序(数据样本需要在固定范围内)

比如输入n个元素,范围是0-k的整数,统计出0-k每个元素有多少个。
好比准备k+1个桶,每出现一个数,就放进对应的桶里,每一个桶的作用就是标记每个数出现的次数。


image.png

基数排序(十进制的数)

将整数按位数切割,然后每个位数分别比较。

   public static void radixSort(int[] arr) {
       if (arr == null || arr.length < 2) {
           return;
       }
       radixSort(arr, 0, arr.length - 1, maxbits(arr));
   }

   /**
    * 最大值有几位
    * @param arr
    * @return
    */
   public static int maxbits(int[] arr) {
       int max = Integer.MIN_VALUE;
       for (int i = 0; i < arr.length; i++) {
           max = Math.max(max, arr[i]);
       }
       int res = 0;
       while (max != 0) {
           res++;
           max /= 10;
       }
       return res;
   }

   //在arr[begin..end]排序
   public static void radixSort(int[] arr, int begin, int end, int digit) {
       final int radix = 10;
       int i = 0, j = 0;

       //有多少个数就准备多少个辅助空间
       int[] bucket = new int[end - begin + 1];
       //有多少位就进出几次
       for (int d = 1; d <= digit; d++) {
           //准备10个空间,count[0]代表当前位(d)是0的数字有多少个
           //count[i]代表当前位是(0到i)的数字有多少个
           int[] count = new int[radix];

           //count[j]:d位置的j的总数
                      //统计对应位(个位、十位、百位等)的每个数字的总数
           for (i = begin; i <= end; i++) {
               j = getDigit(arr[i], d);
               count[j]++;
           }
           //累加,对于每个位数的元素i,统计出小于等于i的元素有几个
           for (i = 1; i < radix; i++) {
               count[i] = count[i] + count[i - 1];
           }
                       
           for (i = end; i >= begin; i--) {
               j = getDigit(arr[i], d);
               bucket[count[j] - 1] = arr[i];
               count[j]--;
           }
           for (i = begin, j = 0; i <= end; i++, j++) {
               arr[i] = bucket[j];
           }
       }
   }

   //获取d位置的数
   public static int getDigit(int x, int d) {
       return ((x / ((int) Math.pow(10, d - 1))) % 10);
   }

稳定性对比

稳定性是指,在排序之后,相同数的相对位置不变
稳定算法:冒泡排序、插入排序、归并排序、基数排序
不稳定算法 :选择排序、快速排序、堆排序

稳定性的意义

如果只是简单的进行数字的排序,那么稳定性将毫无意义。
如果排序的内容仅仅是一个复杂对象的某一个数字属性,那么稳定性依旧将毫无意义
如果要排序的内容是一个复杂对象的多个数字属性,但是其原本的初始顺序毫无意义,那么稳定性依旧将毫无意义。
除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义,那么我们需要在二次排序的基础上保持原有排序的意义,才需要使用到稳定性的算法,例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。(当然,如果需求不需要保持初始的排序意义,那么使用稳定性算法依旧将毫无意义)。

各个算法对比:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,718评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,683评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,207评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,755评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,862评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,050评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,136评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,882评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,330评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,651评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,789评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,477评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,135评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,864评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,099评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,598评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,697评论 2 351

推荐阅读更多精彩内容

  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,713评论 0 13
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,170评论 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,245评论 0 2
  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,459评论 0 15
  • 2016年12月30日 晚20:30对象:拆书家训练营第十一期学员方式:语音+图片以下为分享文字实录 一、我的收获...
    新悦往里往外看阅读 1,285评论 5 6