Java教程

排序算法总结

本文主要是介绍排序算法总结,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

1.冒泡排序

每次比较相邻的元素,每一轮都会把未排序区的最大元素找出来,放到后面。



public void bubbleSort(int[] arr){
    for(int i = 0; i < arr.legnth - 1; i++){
        // 第i轮开始,arr[n-i, n]已排好序
        for(int j = i; j < arr.legnth - i - 1; j++){
            if(arr[j] > arr[j + 1]){
                swap(arr, j, j + 1);
            }
        }
    }
}

2.选择排序

先把小的拿出来,再把最小的拿出来...,每次选择还没处理的元素中最小的元素。

image-20211005155331362

使用索引j扫描一遍数组,找到最小的元素,与第1个元素进行交换,i++

image-20211005155835028

image-20211005160029840

arr[0,i)已排好序,arr[i,n)未排序,把arr[i...n)中的最小值放到arr[i]的位置



public static void sort(int[] arr){
    for(int i = 0; i < arr.length; i++){
        // 选择arr[i...n)中最小值
        for(int j = i; j < arr.length; j++){
            if(arr[j] < arr[i]){
                swap(arr, j, i);
            }
        }
    }
}

3.插入排序

每次处理一张牌,把这张牌插入到前面已经排好序的牌中

image-20211005134206349

i=0,i指向当前处理的元素,无需处理;

image-20211005134328246

i=1,arr[1]=4,使用索引j判断当前元素是否需要插入到前面某一个位置,j初始指向4,j--,元素6比4大,4应该插入到6的前面。

image-20211005134907458

i=2,j=2,arr[j-1]>arr[j],所以当前元素2应该插入到6的前面,j--;

image-20211005135307441

j=1,arr[j-1]>arr[j],2继续插入到4的左边,j=0,此时2已经来到正确的位置,至此前三个元素已经处理完毕。

image-20211005140324842

依次循环,arr[0,i)已排好序,arr[i,n)未排序,把arr[i]放到合适的位置。



public class InsertionSort {
    public static void sort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
//            for (int j = i; j > 0; j--) {
//                if(arr[j-1] > arr[j]){
//                    swap(arr, j-1, j);
//                }else {
//                    break;
//                }
//            }
            for (int j = i; j > 0 && arr [j-1] > arr[j]; j--) {
                swap(arr, j-1, j);
            }
        }
    }
    public static void swap(int[] arr, int i, int j){
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }
}

插入排序小优化:

image-20211005144200315

为了将3到达正确的位置,6和3交换,4和3交换,3和2不交换,每一次交换涉及三次操作;

为了找到3正确的插入位置,可以先用变量t暂存3,此时i=3,j=3,arr[j-1]=6>t,将arr[j-1]=6赋值给arr[j]=3,j--;

image-20211005144815701

j=2,3是否放到此时j=2这个位置呢?arr[j-1]=4>t,将arr[j-1]=4赋值给arr[j]=6,j--;

image-20211005145140334

j=1,arr[j-1]=2<t,所以3应该插入到这个位置,将t赋值给arr[j]=4;



    public static void sort(int[] arr){
        for (int i = 0; i < arr.length; i++) {
            // 将arr[i]插入到合适的位置
            int t = arr[i];
            int j;
            //j从i开始向左扫描,如果arr[j-1]大于t,将arr[j-1]赋值给arr[j]
//            for (j = i; j > 0; j--) {
//                if(arr[j - 1] > t){
//                    arr[j] = arr[j - 1];
//                }else {
//                    break; // 必须写上
//                }
//            }
            // 简化写法
            for(j = i; j > 0 && arr[j - 1] > t; j--){
                arr[j] = arr[j - 1];
            }
            arr[j] = t;
        }
    }

对于有序或近乎有序的数组,插入排序的复杂度是O(n)​,但插入排序整体复杂度仍是O(n2)​;

4.归并排序

分治法:把原问题分解为规模较小的子问题,递归地解决这些子问题,然后合并子问题的结果,得到原问题的解。

合并两个有序区间arr[l, mid]和arr[mid+1, r] , merge(arr, l, mid, r):

QQ20211005-170818

初始化,i = l指向第一个区间初始元素, j = mid + 1指向第二个区间的初始元素,

arr[i]=1<arr[j]=2,取两者更小的元素1赋值给原始数组arr[0],i++

QQ20211005-172643

arr[i]=3<arr[j]=2,把arr[j]=2赋值给原始数组arr[1],j++

QQ20211005-173433

依次循环,继续比较i和j指向的两个元素,取两者更小的元素赋值给arr[k],结果为

image-20211005174110123



public static void merge(arr, l, mid, r){
    // 使用了额外的存储空间,不是原地排序        l l+1... r
    // 将数组arr[l,r+1)的元素拷贝到temp数组中  0  1 ... r-l
    // 在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。
    // 临时内存空间最大也不会超过n个数据的大小,所以空间复杂度是O(n)
    int[] temp = Arrays.copyOfRange(arr, l, r + 1);
    int i = l,j = mid + 1;
    for(int k = l; k <= r; k++){
        // i∈[l,mid],j∈[mid+1, r] 判断索引越界情况
        if(i > mid){ // 说明arr[l, mid]的元素已经赋值完成
            arr[k] = temp[j-l];j++;
        }else if(j > r){ // 说明arr[mid+1, r]的元素已经赋值完成
            arr[k] = temp[i-l];i++;
        }else if(temp[i-l] <= temp[j-l]){
            arr[k] = temp[i-l];i++;
        }else{
            arr[k] = tmep[j-l];j++;
        }
    }
}
public static void sort(int[] arr, int l ,int r){
    if(l >= r) return;
    // int mid = (l + r) / 2;
    int mid = l + (r - l) / 2; // 解决l+r越界
    sort(arr, l, mid);
    sort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

image-20211005205257084

假设对n个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2),merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以归并排序时间复杂度递推式为

T(n)={c if n=12T(n/2)+n if n>1​

通过主方法可以求得T(n)=nlogn​

5.快速排序

arr[l+1...j] < v, arr[j+1...i-1] >= v

image-20211005212046246

选择标定点v=4,初始i=1对应的元素6>v,i++;

i=2,对应的元素5>4,i++;

image-20211005212743820

i=3,对应的元素2小于4,需要扩充arr[l+1...j]的区间,此时j++,交换j和i对应的元素,i++;

image-20211005213256590

i=4,对应的元素3也小于4,同理,j++,交换j和i对应的元素,i++;

image-20211005213533003

i=5,8>4,i++;

i=6,7>4,i++;

image-20211005213737773

i=7,1<4,所以j++,交换j和i对应的元素,i++;

image-20211005213859313

最后交换l和j对应的元素,把标定点的元素放到中间

image-20211005214020247

快速排序递归过程:

image-20211006133919888

完全有序的数组快速排序分析

image-20211005221947130

这种情况时间复杂度是O(n2)​



    private static int partition(int[] arr, int l, int r){
        // 生成[l, r]的随机索引
        int p = l + (new Random()).nextInt(r - l + 1);
        swap(arr, l, p);
        // arr[l+1...j] < v, arr[j+1...i-1] >= v
        int j = l;
        for (int i = l + 1; i <= r; i++) {
            if(arr[i] < arr[l]){
                j++;
                swap(arr, j, i);
            }
        }
        swap(arr, l, j);
        return j;
    }
    public static void sort(int[] arr, int l, int r){
        if(l >= r) return;
        int p = partition(arr, l, r);
        sort(arr, l, p-1);
        sort(arr, p+1, r);
    }

全是0的数组快速排序分析

image-20211006140321843

双路快速排序

循环不变量:arr[l+1...i-1] <= v, arr[j+1...r] >= v

image-20211006142058097

i负责寻找小于等于v的元素,j负责寻找大于等于v的元素

初始i=1,指向元素6>4;初始j=7,指向元素1<4;于是交换元素6和1,i++,j--;

image-20211006142714556

i=2,指向元素5>4,停留在这里;j=6,指向元素7,j--继续寻找小于v的元素,直到停在3的位置。交换元素5和3,i++,j--;

image-20211006143134991

i=3,指向元素2,继续i++后i>j,停止循环。最后交换l和j对应的元素,返回j。

image-20211006143719716

特殊测试用例分析:

image-20211006143907726

i 从左到右扫描遇到第一个大于等于v的元素就停止; j 从右到左扫描遇到第一个小于等于v的元素就停止;

初始i=1指向元素0>=0,j=7指向元素0<=0,交换i和j对应的元素,i++,j--;

image-20211006144224102

i=2,j=6,此时继续交换两者对应的元素,i++,j--;

image-20211006144336180

直到i和j指向同一个元素,停止循环。最后交换l和j对应的元素,返回j



    private static int partition(int[] arr, int l, int r){
        // 生成[l, r]的随机索引
        int p = l + (int)(Math.random() * (r - l + 1));
        swap(arr, l, p);
        // arr[l+1...i-1] <= v, arr[j+1...r] >= v
        int i = l + 1, j = r;
        while (true){
       // 在i<=j的条件下,如果arr[i] < arr[l]就继续循环,否则arr[i] >= arr[l]就停止
            while (i <= j && arr[i] < arr[l])
                i++;
            while (j >= i && arr[j] > arr[l])
                j--;
       // i>j已经把数组中的元素遍历完了;i=j表示指向了一个等于标定点的元素,也不需要处理;
            if(i >= j) break;
            swap(arr, i, j);
            i++;
            j--;
        }
        swap(arr, l, j);
        return j;
    }

三路快速排序

三路快排将数组分为三部分<v,=v,>v;i指向当前待处理元素e,如果e==v,i++;

如果e<v,lt++,将e和此时lt位置的元素交换,i++;

如果e>v,gt--,将e和此时gt位置的元素交换,此时i位置是交换后的元素,仍是未处理的元素。

image-20211006163304880

当i>=gt时,结束循环。lt指向小于v的最后一个元素,gt指向大于v的第一个元素;

image-20211006163757812

最后,交换l和lt位置的元素,此时v就放到了==v部分的第一个元素的位置。这时边界条件也发生了变化。

image-20211006163816415

之后,只需要对<v和>v部分的元素进行递归快速排序就行了。



// 三路快排
public static void sort(int[] arr, int l, int r){
        if(l >= r) return;
        // 生成[l, r]的随机索引
        int p = l + (int)(Math.random() * (r - l + 1));
        swap(arr, l, p);
        // arr[l + 1, lt] < v, arr[lt + 1, i - 1] == v, arr[gt, r] > v
        int lt = l, i = l + 1, gt = r + 1;
        while (i < gt){
            if(arr[i] < arr[l]){
                lt++;
                swap(arr, i, lt);
                i++;
            }else if(arr[i] > arr[l]){
                gt--;
                swap(arr, i, gt);
            }else {
                i++;
            }
        }
        swap(arr, l, lt);
        // arr[l, lt - 1] < v, arr[lt, gt - 1] == v, arr[gt, r] > v
        sort3(arr, l, lt - 1);
        sort3(arr, gt, r);
    }

6.堆和堆排序

普通的队列是先进先出原则;优先队列则是按照优先级高低进行出队,比如将优先级最高的元素作为队头元素优先出队。

应用场景:医院门诊,优先看病情重的病人; 游戏中优先攻击最近单位,优先攻击血量最低等;售票窗口的老幼病残孕和军人优先购票等。

完全二叉树:最后一层的叶子节点都靠左排列,其他层的节点个数都要达到最大。

堆是一个完全二叉树,常用的存储方式是数组。对于每个节点的值都大于等于子树中每个节点值的堆,叫做最大堆。对于每个节点的值都小于等于子树中每个节点值的堆,叫做最小堆。

image-20211006193924483

从数组索引0开始存储节点,设节点x存储在数组中下标为i的位置,则其左子节点下标为2i+1,右子节点下标为2i+2,父节点为(i-1)/2。

添加元素和siftUp

现在直接向数组中添加一个元素52,就不满足最大堆的性质了。因此,我们需要对52和其父节点做一些调整,来维护最大堆的性质。

image-20211006200501675

具体方法是依次比较新添加元素和其父节点的大小,不断交换位置。这里52>16,交换52和16的位置,

image-20211006201450759

交换后52>41,继续交换52和41的位置。52<62,这时就完成了调整工作。

image-20211006201922055



public void add(E e){
    data.add(e);
    siftUp(data.size()-1);
}
private void siftUp(int k){
    while(k > 0 && data.get(k).compareTo(data.get(parent(k))) > 0){
        data.swap(k, (k - 1) / 2); // 交换两个索引对应的元素
        k = (k - 1) / 2;
    }
}

删除堆顶(取出最大)元素和siftDown

首先将数组最后一个元素16调整到堆顶,然后删除元素62。但此时不满足堆的性质,需要继续调整。

image-20211006203618851

依次将元素16和其最大的孩子做交换,直到满足堆的性质。

第一次交换

image-20211006204035903

第二次交换,调整完成。

image-20211006204346407

一个包含n个节点的完全二叉树,树的高度不会超过log2⁡n​,siftDown和siftUp过程是顺着节点所在路径比较交换的,所以两者的时间复杂度跟树的高度成正比,都是log⁡n​

HeapifyO(n)

从最后一个非叶子节点开始,向前遍历,对每一个节点进行siftDown操作。

image-20211006214623627

蓝色节点表示叶子节点,红色节点22表示最后一个非叶子节点。(最后一个非叶子节点的索引等于数组中最后一个元素父节点的索引)

首先对22进行siftDown操作,22只有一个左孩子且22<62,所以交换22和62的位置,交换后22是叶子节点,调整完成。

image-20211006220413675

继续对13调整,结果如图所示

image-20211006220503016

依次循环,不断调整

image-20211006220957199

原地堆排序



public static void heapSort(int[] data){
        if(data.length <= 1) return;
        // heapify操作:从最后一个非叶子节点开始,对每一个节点siftDown操作
        for (int i = (data.length - 2) / 2; i >= 0; i--) {
            siftDown(data, i, data.length);
        }
        for (int i = data.length - 1; i >= 0; i--) {
          // 将堆顶元素放到最后一个位置,i--,次大的元素放到倒数第二个位置...
            swap(data,0, i); 
            siftDown(data, 0, i);
        }
    }
    // 对data[0, n)形成的最大堆中索引k的元素执行siftDown操作
    private static void siftDown(int[] data, int k, int n){
        while(2 * k + 1 < n){
            int j = 2 * k + 1; // j是k节点对应的左孩子索引,j+1是k节点对应的右孩子索引
            // j+1<n说明k节点有右孩子
            if(j + 1 < n && data[j + 1] > data[j])
                j++; // 此时j存储的是k节点的右孩子索引
            // 如果k节点元素值 >= 左右孩子中最大元素的值,满足堆的性质,结束循环
            if(data[k] >= data[j])
                break;
            swap(data, k, j); // 交换k,j索引对应的元素值
            k = j; // 更新k
        }
    }
    private static void swap(int[] arr, int i, int j){
        int t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
    }

Top K问题

LeetCode 215. 数组中的第K个最大元素

剑指 Offer 40. 最小的k个数

原地排序:不申请额外的存储空间进行排序;

稳定排序:待排序的序列中存在值相等的元素,排序之后,保持相等元素先后顺序不变。

排序算法时间复杂度空间复杂度稳定性
冒泡排序O(n2)​O(1)​稳定
选择排序O(n2)​O(1)​不稳定
插入排序O(n2)​O(1)​稳定
归并排序O(nlogn)​O(n)​稳定
快速排序O(nlogn)​取决于递归次数不稳定
堆排序O(nlogn)​O(1)​不稳定

 

冒泡排序、插入排序、选择排序算法的运行并不需要额外的存储空间,空间复杂度都是 O(1),它们都是原地排序算法。

选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。比如 3,3,2这样一组数据,第一次找到最小元素 2,与第一个3交换位置,那第一个3和中间的3顺序就变了,所以就不稳定了。

对于快速排序,如果输入数组中有两个相同的元素,比如数组{3,2,2,5}在经过第一次分区操作之后,两个2的相对先后顺序就会改变。所以快速排序不是稳定的。

对于堆排序,在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。

在java中,Arrays.sort方法对数组进行排序是一个综合的排序:

  • 一般来讲,如果数组是基本类型的,那么直接选择快速排序,虽然不具有稳定性,但是对于每个数据来讲,并没有其他的意义,只是单纯的数字而已。

  • 如果数组是引用类型的,那么选择归并排序,因为引用类型不是单纯的数字,应该选择具有稳定性的排序方式。

  • 如果数组的长度是小于47(待定)的,直接选择插入排序,因为插入排序的常数操作少。

这篇关于排序算法总结的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!