常见排序算法及其代码实现和相关问题

主宰稳场 提交于 2020-02-23 03:43:52

算法学习笔记—排序

常见的一些数组排序方法

冒泡排序

  • 冒泡排序的原理:
  • 相邻位进行比较,如果满足前面的数大于后面的数,就交换前后两个数。排一个来回之后,数组中最大的数已经被移动到了最后面。
  • 时间复杂度的分析:N+(N-1)+(N-2)+… 等差数列,O(N2)
  • 冒泡排序的代码实现:
public void bubbleSort(int[] arr){
    if(arr == null || arr.length < 2){
        //数组为空或者数组中只有一个元素,不需要进行排序
        return ;
    }
    for(int end = arr.length - 1; end > 0; end--){
        //使用end来标记当前比较移动操作的最后一位是多少。
        //第一波比较从索引0到索引arr.length
        //第二波比较从索引0到索引arr.length-1
        for(int i = 0;i < end;i++){
            //如果前一个数大于后一个数,就交换两个数,将大的数移动到后面
            if(arr[i]>arr[i+1]){
                swap(arr,i,i+1);
            }
        }
    }
}
public void swap(int[] arr,int i, int j){
 	//交换arr[i]和arr[i+1]
    int temp = arr[i];
    arr[i] = arr[i+1];
    arr[i+1] = temp;
}

选择排序

  • 选择排序的思想:
  • 从0到N-1找一个最小的放在索引0处,从1到N-1找最小的放在索引1处,一直到最后N-2处,索引N-1处时只剩下一个元素,不需要进行排序。
  • 时间复杂度的分析:O(N2)
  • 选择排序的代码实现:
public void selectionSort(int[] arr){
    if(arr==null||arr.length<2){
        return ;
    }
    //外层循环控制比较操作的开始索引
    for(int begin = 0; begin < arr.length-1; begin++){
        //内层循环找到外层指定范围内的最小值
        for(int j = begin + 1;j< arr.length;j++){
            if(arr[j]<arr[begin]){
                //如果当前遍历到的值小于索引begin处的值,就交换
                swap(arr,begin,j);
            }
        }
    }
}

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

插入排序

  • 插入排序的思想:
  • 你手中握着一堆牌,已经排序好了,现在新抓一张牌,需要将新抓的牌放到一个合适的位置,逐个和前面的牌进行比较。
  • 时间复杂度的分析:O(N2)
  • 插入排序的代码实现:
public 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);
        }
    }
}
public void swap(int[] arr,int i,int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
  • 插入排序实际运行时间,是和数据的状况有关系的,选择排序和冒泡排序的时间复杂度与输入数据无关:
  • 如果输入的数据本身就是升序的,那么最终需要的时间是O(N),这是最好的情况。
  • 而如果输入的数据本身是降序的,那么最终需要的时间就是O(N2),这是最坏的情况。

归并排序

  • 归并排序的思想:
  • 将数组分为两个部分,分别将两个部分排序,形成两个有序子数组,在生成一个工具数组,利用外排序的方法,将两个数组排序后填进工具数组中,将工具数组拷贝到源数组中。
  • 时间复杂度分析:O(N*logN) 额外空间复杂度O(N)
  • 代码实现:
public void sortProcess(int[] arr,int L, int R){
    if(L==R){
        //终止条件
        return;
    }
    int mid = (L+R)/2;
    //求中点位置
    sortProcess(arr,L,mid);
    sortProcess(arr,mid+1,R);
    merge(arr,L,mid,R);
    //此时,左半部分已经拍好序了,右半部分也排好了,但整体是无序的,需要进行外排序
}
public void merge(int[] arr,int L,int mid, int R){
    //实现外排序
    int[] help = new int[R-L+1];
    //工具类
    int p1 = L;
    //左指针
    int p2 = mid+1;
   	//右指针
    int index = 0;
    while(p1<=mid&&p2<=R){
        help[index++] = arr[p1]< arr[p2]? arr[p1++]:arr[p2++];
    }
    //两个指针必然有一个越界
    while(p1<=mid){
        help[index++] = arr[p1++];
    }
    while(p2<=R){
        help[index++] = arr[p2++];
    }
    //工具数组已经排序完毕,将工具数组拷贝至原数组
    for(int i = 0;i<help.length;i++){
        arr[L+i] = help[i];
    }
}
public void mergeSort(int[] arr){
    if(arr == null|| arr.length<2){
        return;
    }
    sortPress(arr,0,arr.length-1);
}

快速排序

  • 快速排序的思想:
    • 以数组的在最后一个值作为边界值,做到小于此值的数在数组的前面,大于此值的数在数组的后面。则左部分和右部分都是相对选择的特定值有序的,迭代此过程,直至整个数组有序。
  • 依照荷兰国旗问题来改进快速排序的思想:
    • 还是一数组的最后一个值作为边界值,小于放在前面,等于放在中间,大于放在后面,将等于部分固定不动,前半部分和后半部分继续迭代此过程。
  • 代码实现:
public void quickSort(int[] arr){
    if(arr ==null||arr.length<0){
            return;
    }
    quickSort(arr,0,arr.length-1);
}
public void quickSort(int[] arr,int L,int R){
    if(L<R){
        int[] p = partition(arr,L,R);
        quickSort(arr,L,p[0]-1);
        quickSort(arr,p[1]+1,R);
        }
}
public int[] partition(int[] arr,int L,int R){
    int less = L-1;
        int more = R;
        while(L<more){
            if(arr[L]<arr[R]){
                //R固定不动,使用L来遍历整个数组
                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};
}

随机快速排序

  • 快速排序的问题所在在于每次都固定数组的最后一个位置作为划分的边界值num,也就是说,非常容易找到反例使得快速排序的问题时间复杂度变差,最佳情况下是每一次都能够将数组进行2等分,此时算法的时间复杂度为O(N*logN),而最差情况下的时间复杂度就会变为O(N2)
  • 随机快速排序的出现就是为了改进快速排序的固定边界值的思想,改用随机选择边界值来代替。其余的思想是一致的。
  • 随机快速排序的时间复杂度将与随机选择的位置有关。O(NlogN)。

堆排序

  • 在介绍堆排序之前需要先认识堆结构,而认识堆结构需要先了解什么是完全二叉树。
  • 完全二叉树包括满二叉树和叶子结点按照从左到右填的二叉树。
  • 堆就是完全二叉树,堆分为大根堆和小根堆:
    • 大根堆,二叉树中每一颗子树的最大值,都是该子树的头结点。
    • 小根堆,二叉树中每一颗子树的最小值,都是该子树的头结点。
  • 要实现堆排序,需要先将数组变成堆结构,堆结构是逻辑上的结构,可以使用数组来实现:
    • 一个数的索引为index,则其父节点的索引为(index-1)/2,其左孩子的索引为2*index+1,右孩子的索引为2*index+2
  • 将一个数组调整为逻辑中的大根堆,其时间复杂度可以用log1+log2+log3+…+log(n-1)来估计,最终结论为O(N).
  • 代码实现:
public void main(String[] args){
    for(int i = 0;i<arr.length,i++){
        //向对中添加元素,经过此次循环,整个数组变为大根堆结构
        heapInsert(arr,i);
    }
}
public void heapInsert(int[] arr,int index){
    while(arr[index]>arr[(index-1)/2]){
        //当前孩子节点的值大于父节点的值,交换两个数
        swap(arr,index,(index-1)/2);
        //向上迭代
        index = (index-1)/2;
    }
}
public void swap(int[] arr,int i ,int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
  • heapify过程,当一个堆已经形成,但对中某一个元素值变小了,导致的当前堆并不是大根堆了,需要对此堆进行相应的调整过程。
  • heapify应用于减堆操作,当把堆顶元素弹出时,为保证堆的结构不受破坏,就需要将堆顶元素和堆的最后一个元素,索引为heapSize-1的元素交换,将heapSize-1,然后对堆顶元素进行heapify操作。
  • heapify的思路,当前值变小,就找到当前值的左孩子和右孩子,找到相对大的那一个,然后将当前值与之进行交换,不断向下迭代,直至索引越界。
  • 代码实现:
public void heapify(int[] arr,int index, int heapSize){
    int leftChild = 2 * index + 1;
    while(leftChild < heapSize){
        //如果右孩子越界,则最大值为左孩子,如果右孩子不越界,则最大值是两个孩子之间的最大值。
        int maxIndex = leftChild + < heapSize && arr[leftChild + 1]>arr[leftChild]?
            leftChild + 1 :leftChild;
        maxIndex = arr[maxIndex] > arr[index] ? maxIndex : index;
        if(maxIndex == index ){
            break;
        }
        swap(arr,maxIndex,index);
        index = maxIndex;
        leftChild = index * 2 + 1;
    }
}
  • 已经了解了heapInsertheapify的过程,那么就可以实现堆排序,堆排序的思想就是:
    • 先将一个数组变成一个大根堆。
    • 将大根堆的头结点和数组最后一个元素交换,然后将heapSize-1,对新的头结点进行heapify操作。
    • 迭代上述过程,最终实现正序的排序。
  • 代码实现:
public void heapSort(int[] arr){
    if(arr == null || arr.length <2){
        return;
    }
    //构建堆
    for(int i = 0;i<arr.length;i++){
        heapInsert(arr,i);
    }
    int heapSize = arr.length;
    //交换堆顶元素和最后一个元素
    swap(arr,0,--heapSize);
    while(heapSize>0){
        heapify(arr,0,heapSize);
        //交换堆顶元素和最后一个元素
        swap(arr,0,--heapSize);
    }  
}

public void heapInsert(int[] arr, int index){
    //对于每一个新插入的元素,都需要跟其父节点对比大小
    while(arr[index]>arr[(index-1)/2]){
        swap(arr,index,(index-1)/2);
        index = (index-1)/2;
    } 
}
public void swap(int[] arr,int i,int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
public void heapify(int[] arr,int index,int heapSize){
    int leftChild = index * 2+1;
    while(leftChild < heapSize){
        int maxIndex = leftChild +1 <heapSize && arr[leffChild+1] > arr[leftChild] ?
            leftChild+1:leftChild;
        maxIndex = arr[index] > arr[maxIndex] ? index : maxIndex;
        if(maxIndex == index){
            break;
        }
        swap(arr,index,maxIndex);
        index = maxIndex;
        leftChild = index * 2 + 1;
    }
}

桶排序

  • 桶排序是一个概念,具体的实现可以分为计数排序和基数排序,在一直数据的情况下,使用桶排序是非常快的。桶排序的思想是,初始化一些桶,用桶来对数组中出现的每个数进行统计,将每个数都划分到其对应的桶中。
  • 计数排序是桶排序的一种,统计每个数出现的频率,然后在从大到小恢复数组,不需要比较数的大小。
  • 基数排序使用十个桶,依次比较各个位。
  • 桶排序的时间复杂度为O(N),空间复杂度为O(N)。具有稳定性。

排序方法的稳定性

  • 排序的稳定性定义:
    • 如果一个排序方法在排序的过程中,不会破坏相同数据的原有顺序,则这个排序方法是具有稳定性的,否则就没有稳定性。
  • 几种排序算法的稳定性分析:
    • 冒泡排序:具有稳定性
    • 插入排序:具有稳定性
    • 快速排序:不具有稳定性
    • 选择排序:不具有稳定性
    • 归并排序:具有稳定性
    • 堆排序:不稳定
  • 在java的API中,sort方法对数组进行排序是一个综合的排序:
    • 一般来讲,如果数组是基本类型的,那么直接选择快速排序,虽然不具有稳定性,但是对于每个数据来讲,并没有其他的意义,只是单纯的数字而已。
    • 如果数组是引用类型的,那么选择归并排序,因为引用类型不是单纯的数字,应该选择具有稳定性的排序方式。
    • 如果数组的长度是小于60的,直接选择插入排序,因为插入排序的常数操作少。

比较器

  • 在比较引用类型中对象的某一个属性时,需要自定义一个比较器,用于指定比较对象的哪一个属性。
  • 代码实现:
public static void main(String[] args){
    Student s1 = new Student(1,"xiaoming",10);
    Student s2 = new Studeng(2,"xiaofang",11);
    Student s3 = new Student(3,"xiaoli",18);
    Student[] students = new Student[] {s1,s2,s3};
    //直接进行排序,由于传入的引用类型,所以会直接按照对象的内存地址进行排序,毫无意义
    Arrays.sort(students);
    //自定义比较器传入,按照每个对象的id进行排序
    Arrays.sort(students,new IdDesendingComparator());
}
public static class Student{
    int id;
    String name;
    int age;
    public Student(int id,String name,int age){
        this.name = name;
        this.age = age;
        this.id = id;
    }
}
public static class IdDescendingComparator implements Comparator<Student>{
    @Override
    public int compare(Student t1, Student t2){
        return t1.id - t2.id;
        //当返回一个负数时,t1在前
        //当返回一个正数时,t2在前
        //当返回0时,t1和t2并列
    }
}
  • 在使用系统提供的有序结构时,都需要用到比较器,使得系统知道使用哪种指定的顺序来组织这个结构。

递归的实质

  • 递归的过程在逻辑上来讲就是自己调用自己的过程。
  • 递归的过程在系统上来讲当程序运行时,遇到调用自己的子过程,就将此时所有的信息保存,包括程序运行到第几行,程序产生了哪些变量,等等压在系统栈中,一直重复此过程,直至程序运行到停止条件为止,然后将栈中保存的信息取出,根据保存的信息恢复现场继续进行,当前子过程运行结束后,再从栈中取出信息,继续进行,直到栈为空。
  • 任何的递归行为都可以用非递归实现。
  • 递归程序的时间复杂度判断:–master 公式
  • 凡是满足T(N) = a*T(N/2) +O(N^d)的程序,若:
    • log(b,a) > d -->复杂度为O(N^log(b,a))
    • log(b,a) = d -->复杂度为O(N^d*logN)
    • log(b,a) < d -->复杂度为O(N^d)
  • 其中log(b,a)表示以b为底的a次幂
  • master 公式的条件就是划分的子过程的规模是一致的,每一次都是二分的。

对数器

  • 何谓对数器,对数器就是验证自己所设计的算法功能是否正确。
  • 对数器的设计需要:
    • 先写一个结果必定正确但时间复杂度低的算法。
    • 随机生成测试样本。
    • 将随机生成的样本同时喂给两个算法。
    • 设计一个isEqual的算法来验证两个算法的结果是否保持一致,如不一致,打印出此时的测试用例。
    • 根据测试用例来调试自己的算法。

和排序相关的经典问题

小和问题

  • 问题描述:在一个数组中,每个数左边的比当前数小的数累加起来,称为这个数组的小和,求一个数组的小和。
  • 例子:
    • [2,4,6,3,1]
    • [2]前面没有比它小的,小和为0
    • [4]前面2比它小,小和为0+2
    • [6]前面4和2比它小,小和为0+2+2+4
    • [3]前面2比它小,小和为0+2+2+4+2
    • [1]前面没有比它小的,小和为0+2+2+4+2
    • 数组的小和为10
  • 小和问题的求解思路:
    • 暴力破解,对于每一个位置依次将它前面的数进行遍历
    • 使用归并排序的思路解决,前面使用归并的思路对数组进行排序,排序后的每一个子序列都是有序的,那么对于序列中的数字来说,通过外排的方式排序时,就能够通过索引直接得到比当前值大的数有多少个,有多少个那么当前数对小和产生的影响就有多少。
    • 使用归并排序解决小和问题的例子:
    • [1,2,3,4]–>[1,2]+[3,4],在外排时,对于元素1,右面3和4都比它大,所以元素1对小和的影响就是1×2,对于元素2,一样的产生的影响是2×2。
  • 代码实现:
public class Code_smallSum{
    public int smallSum(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        return mergeSort(arr,0,arr.length-1);
    }
}
public int mergeSort(int[] arr,int L,int R){
    if(L==R){
        return 0;
    }
    int mid = (L+R)/2;
    // int mid = L+(R-L)>>1;
    //位运算的速度要比常数运算的速度快的多。a>>1 == a/2
    //整体的小和=左侧子列产生的小和+右侧子列的小和+在merge过程中产生的小和
    return mergeSort(arr,L,mid) + mergeSort(arr,mid+1,R) + merge(arr,L,mid,R);
}
public int merge(int[] arr,int L,int mid,int R){
    int[] help = new int[R-L+1];
    int index = 0;
    int p1 = L;
    int p2 = mid+1;
    int res = 0;
    while(p1<=mid && p2 <= L){
        //求出左侧子列在归并过程中对右侧子列来讲,产生多少的小和
        res += arr[p1]< arr[p2] ? (R-p2+1) * arr[p1] : 0;
        help[index++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
    }
    while(p1<=m){
        help[index++] = arr[p1++];
    }
    while(p2<=R){
        help[index++] = arr[p2++]; 
    }
    for(int  i = 0;i< help.length;i++){
        arr[L+i] = help[i];
    }
    return res;
}

逆序对问题

  • 问题描述:在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对。
  • 逆序对问题的求解思路:
    • 暴力破解,对于每一个数来说,都依次进行遍历
    • 归并排序解决,跟小和问题的思路一致,只不过小于变成了大于,求和变成了打印。
  • 代码实现:
public void ReverseOrderPairs(int[] arr){
    if(arr == null || arr.length < 2){
        return;
    }
    mergeSort(arr,0,arr.length-1);
}
public void mergeSort(int[] arr,int L,int R){
    if(L == R){
        return;
    }
    int mid = (L+R)/2;
    mergeSort(arr,L,mid);
    mergeSort(arr,mid+1,R);
    merge(arr,L,mid,R);
}
public void merge(int[] arr,int L,int mid,int R){
    int[] help = new int[R-L+1];
    int index = 0;
    int p1 = L;
    int p2 = mid+1;
    while(p1<=mid && p2<=R){
        if(arr[p1]>arr[p2]){
            int temp = p2;
            while(p2<=R){
                System.out.printf("逆序对%d--%d",arr[p1],arr[temp++]);
            }
            help[index++] = arr[p1++];
        }else{
            help[index++] = arr[p2++];
        }
    }
    while(p1<=mid){
        help[index++] = arr[p1++];
    }
    while(p2<=R){
        help[index++] = arr[p2++];
    }
    for(int i = 0;i<help.length;i++){
        arr[L+i] = help[i];
    }
}

荷兰国旗问题

  • 简单版:

    • 给定一个数组,和一个数num,情把小于num的数放在数组的左边,大于num的数放在数组的右边。
    • 要求时间复杂度O(N),空间复杂度O(1)。
  • 进阶版:

    • 给定一个数组和一个数num,小于num的放在左边,等于的放中间,大于的放右面。
    • 要求时间复杂度O(N),空间复杂度O(1)。
  • 荷兰国旗问题的求解思路:

    • 设置一个指针,用于指定在当前状态下,所有满足小于num的数字的区域,当遍历下一个数字时,如果大于num,不做任何操作,如果小于num,那就将此数字移动到指针指定索引的下一个位置,并将指针自增1,使得满足小于num的区域扩增。
    • 对于进阶版的求解思路,和上面的类似,设置两个指针,一个less,一个more,当遍历到的数字等于num时,不做操作,小于num就将less+1,并且与less指定位置的数互换,大于num的话就讲more-1,并与more指定位置的元素互换。
  • 代码实现:

public class CodeNetherlandsFlag(int[] arr,int num){
    int less = -1;
    int more = arr.length;
    int p = 0;
    while(p<more){
        if(arr[p] == num){
            //当前值与num相等,不做操作,遍历下一个
            p++;
        }else if(arr[p]<num){
            //当前值小于num,将当前值移动到less+1处
            swap(arr,++less,p++);
        }else {
            swap(arr,more--,p);
        }
    }
}

最大间隔问题

  • 现有一个数组,求数组经过排序之后相邻元素之间的最大差值。maxGap问题。
  • 求解思路:借用桶排序的思想,数组的长度为len,则初始化len+1个桶,每个桶具有三个属性,布尔型表征桶中是否有数,两个整形表征桶中的最大值和最小值。
  • 遍历一遍数组,求出数组中的最大值和最小值。将最小值装在第一个桶里,将最大值装在最后一个桶里。将最小值和最大值之间分成len+1份。
  • 再次遍历这个数组,每个数放到符合其范围的桶中,更新桶的最大值和最小值。
  • 遍历所有的桶,可以知道相邻两个差距最大的数肯定不会出现在同一个桶中。
  • 代码实现:
public int maxGap(int[] arr){
    if(arr == null || arr.length < 2){
        return 0;
    }
    int len = arr.length;
    int min = Integer.MAX_VALUE;
    int max = Integer.MIN_VALUE;
    for(int i = 0;i<len;i++){
        min = Math.min(min,arr[i]);
        max = Math.max(max,arr[i]);
    }
    if(max == min ){
        return 0;
    }
    //标志桶中是否有数字
    boolean[] hasNum = new boolean[len+1];
    //标志桶中的最大值
    int[] maxs = new int[len+1];
    //标志桶中的最小值
    int[] mins = new int[len+1];
    int bid = 0;
    for(int i =0;i<len;i++){
        //求数组中的数应当属于哪个桶
        bid = bucket(arr[i],len,min,max);
        //更新该桶的最大值和最小值
        mins[bid] = hasNum[bid] ? Math.min(mins[bid],arr[i]):arr[i];
        maxs[bid] = hasNum[bid] ? Math.max(maxs[bid],arr[i]):arr[i];
        hasNum[bid] = true;
    }
    
    int res = 0;
    //标记最后一个出现的最大值
    int lastMax = maxs[0];
    for(int i =1;i<=len;i++){
        if(hasNum[i]){
            //已有最大的gap和当前gap的比较,更新
            res = Math.max(res,mins[i] - lastMax);
            lastMax = maxs[i];
        }
    }
    return res;
}
  • 将数组中的数分配到对应的桶中。
public int bucket(long num,long len,long min,long max){
   	//判断一个数应当属于哪一个桶
    return (int)((num-min)*len/(max - min));
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!