复习-排序

前言:仅用来自己复习,如果有读者发现错误,提出批评指正,非常感谢。

选择排序

选择排序:假设我们把数组分成有序区间和无序区间,有序区间开始时为0,无序区间为数组元素总个数,选择排序就是不断循环遍历无序区间。找到无序区间最值元素下标,再与无序区间第一个元素交换,使的有序区间越来越大,无序区间越来越小,最后整个区间有序。简而言之,选择排序就是遍历选择最值下标的过程。

public class SelectionSort {

    public static void selectionSort(int[] arr){
        if(arr == null || arr.length < 2){//首先判断数组是否为空,并且元素个数不能小于2,否则没有排序意义
            return;
        }
        for(int i = 0; i < arr.length - 1;i++){//遍历数组,只需要遍历n-1次即可
            int minIndex = i;//获取最小下标,先设为i
            for(int j = i + 1;j < arr.length;j++){//从i+1开始遍历,直到最后
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;//判断当前位置元素与minIndex下标的值大小,并赋值给minIndex
            }
            swap(arr,i,minIndex);//交换i和minIndex两个位置的元素
        }
    }

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

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        selectionSort(arr3);
        for (int n: arr4) {
            System.out.print(n+" ");
        }
    }
}

冒泡排序

冒泡排序:假设我们将数组分为无序区间和有序区间,循环遍历无序区间,依次将相邻两个位置元素大小进行比较,满足排序条件则交换,直到将最值元素冒泡到无序区间最后,使得无序区间越来越小,有序区间越来越大,最后整个区间变得有序。与选择排序比较起来,冒泡排序在每一次遍历,两两比较并交换的过程,其实就是相当于在做排序,而选择排序是每次只交换最值元素。

public class BubbleSort {

    public static void bubbleSort(int[] arr){
        if(arr == null || arr.length < 2){//前提
            return ;
        }
        for(int i = arr.length - 1; i > 0;i--){//循环遍历,n-1次
            for(int j = 0; j < i;j++){
                if(arr[j] > arr[j+1]){
                    swap(arr,j,j+1);
                }
            }
        }
    }

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

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        bubbleSort(arr4);
        for (int n: arr4) {
            System.out.print(n+" ");
        }
    }
}

插入排序

插入排序适用于数据量较小的排序。假设将整个区间分为有序区间和无序区间,遍历整个数组,将无序区间的首元素插入到有序区间有合适位置,最后使得整个区间有序。

//插入排序
public class InsertionSort {

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

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

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        insertSort(arr4);
        for (int n: arr4) {
            System.out.print(n+" ");
        }
    }
}

异或运算寻找奇数次元素

异或运算满足交换律和结合律,并且0^n = n,n^n = 0,这个知识是解决问题的关键

比较常见的两种题型:

第一种题型:找出数组中只有一个数字出现奇数次,其他都为偶数次的数。

解决方案,将0和所有数组元素进行异或运算,最后结果就是出现奇数次的数

第二种题型:找出数组中只有两个数组出现奇数次,其他都为偶数次的两个数
解决方案,先将0和所有数组元素进行异或运算,最后结果就为ab,又因为a!=b所以ab必将出现1位数字为1,此时将数组分为该位数字为1或0的两种数,再将0与其中一类所有数字异或就可以得出a或者b,最后再该结果与a^b进行异或,就可以得出另一个数了

//找数组中出现奇数次的数
public class OddTimesEvenTimes {
    
    public static void printOddTimesNum1(int[] arr){//异或运算解决问题
        int result = 0;
        for(int n : arr){
            result ^= n;
        }
        System.out.println(result);
    }

    public static void printOddTimesNum2(int[] arr){
        int result1 = 0;
        for(int n : arr){
            result1 ^= n;
        }

        int rightOne = result1 & (~result1 + 1); //先将a^b取反,再加1,最后与a^b相与,得出最右位为1的结果
        int result2 = 0;
        for(int n : arr){
            if((rightOne & n) != 0){//不能==1,因为1出现的位置并不一定是最低位,那么即使是都在相同为出现1了,相与结果也不一定为1
                result2 ^= n;
            }
        }
        System.out.println(result2+" "+(result2^result1));
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1,1,5,5,1,2,2,3,4,4,6,6,1};
        int[] arr2 = new int[]{1,1,5,5,1,2,2,3,4,4,6,6};
        printOddTimesNum1(arr);
        printOddTimesNum2(arr2);
    }
}

归并排序

归并排序在我认为更像是在做二分,整体思想就是将整个区间划分为两个,并分别对其进行排序,最后再合并的过程。

public class MergeSort {

    //整体过程
    public static void process(int[] arr,int l,int r){
        if(l == r){//如果l==r了,说明当前区间只有一个数,也就不需要进行排序了。
            return;
        }
        int m = (l+r)/2;
        process(arr,l,m);
        process(arr,m+1,r);
        merge(arr,l,m,r);
    }

    //合并过程
    private static void merge(int[] arr, int l, int m, int r) {
        //开辟r-l+1个元素大小的空间,用来合并两个有序区间
        int[] temps = new int[r-l+1];
        //用两个指针分别去遍历这两个有序区间
        int lCur = l;
        int rCur = m+1;
        int i = 0;
        while(lCur <= m && rCur <= r){
            temps[i++] = arr[lCur] < arr[rCur] ? arr[lCur++] : arr[rCur++];
        }
        while(lCur <= m){
            temps[i++] = arr[lCur++];
        }
        while(rCur <= r){
            temps[i++] = arr[rCur++];
        }
        //再将排好序的数组重新放入原数组中
        for(int j = 0;j < temps.length;j++){
            arr[l+j] = temps[j];
        }
    }

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        process(arr4,0,arr4.length-1);
        for (int n: arr4) {
            System.out.print(n+" ");
        }
    }
}

归并排序拓展

小和问题

在一个数组中,每个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例如:[1,3,4,2,5] 1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;5左边比5小的数,1、3、4、2;所以该数组小和为1+1+3+1+1+3+4+2=16

解决方案:除了暴力解法以外,我们还可以使用归并排序来解。小和是求某个数左边比其小的数之和,那么换个思路,也就是求某个数右边比其大的个数。

public class SumNum {
    public static int sumNum(int[] arr){
        if(arr == null || arr.length < 2){
            return 0;
        }
        return process(arr,0,arr.length-1);
    }

    private static int process(int[] arr, int l, int r) {
        if(l == r)return 0;
        int m = (l+r)/2;
        return process(arr,l,m)
                +process(arr,m+1,r)
                +merge(arr,l,m,r);
    }

    private static int merge(int[] arr, int l, int m, int r) {
        int[] temps = new int[r-l+1];
        int re = 0;
        int p1 = l;
        int p2 = m+1;
        int i = 0;
        while(p1 <= m && p2 <= r){
            re += arr[p1] < arr[p2] ? (r-p2+1)*arr[p1] : 0;
            temps[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while(p1 <= m){
            temps[i++] = arr[p1++];
        }
        while(p2 <= r){
            temps[i++] = arr[p2++];
        }
        for(i = 0;i < temps.length;i++){
            arr[l+i] = temps[i];
        }
        return re;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1,3,4,2,5};
        System.out.println(sumNum(arr));
    }
}

逆序对问题

在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有的逆序对。

解决方案:有了上面小和问题的理解,那么这道题就很简单了,同理!

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

    private static void process(int[] arr, int l, int r) {
        if(l == r){
            return;
        }
        int m = (l+r)/2;
        process(arr,l,m);
        process(arr,m+1,r);
        merge(arr,l,m,r);
    }

    private static void merge(int[] arr, int l, int m, int r) {
        int[] temps = new int[r-l+1];
        int i = 0;
        int p1 = l;
        int p2 = m+1;
        while(p1 <= m && p2 <= r){
            if(arr[p1] > arr[p2]){
                int j = p2;
                while(j <= r) {
                    System.out.println(arr[p1]+","+arr[j++]);
                }
                temps[i++] = arr[p1++];
            }else{
                temps[i++] = arr[p2++];
            }
        }
        while(p1 <= m){
            temps[i++] = arr[p1++];
        }
        while(p2 <= r){
            temps[i++] = arr[p2++];
        }
        for(i = 0;i < temps.length;i++){
            arr[l+i] = temps[i];
        }
    }

    public static void main(String[] args) {
        int[] arr = new int[]{3,2,4,5,0};
        reversedOrder(arr);
    }
}

快速排序

快速排序是寻找一个基准值,把小的放在左边,大的放在右边,依次二分,使得整个区间变得有序。它的时间复杂度的关键就在于partition的过程,也就是寻找基准值并进行排大小的过程,下面这段代码还是有些粗糙了,可以再详细去查一下快排,会有很多种partition的变形,从而达到提高效率的目的。

public class QuickSort {
    public static void quickSort(int[] arr,int l,int r){
        if(l < r){
            swap(arr,l+(int)(Math.random()*(r-l+1)),r);//随机抽取区间内任意一个元素作为基准值,并放在区间末尾
            int[] p = partition(arr,l,r);//整个区间进行partition,使得整个区间分为小于、等于、大于三个区间
            quickSort(arr,l,p[0]-1);//对小于区间继续进行快排
            quickSort(arr,p[1]+1,r);//对大于区间继续进行快排
        }
    }

    private static int[] partition(int[] arr, int l, int r) {
        //创建两个指针,分别代表小于区间右指针和大于区间做指针
        int less = l-1;//默认是该区间最左边的前一个位置
        int more = r;//默认是该区间最右边的位置,因为最右边是基准值,不算做需要去比较的区间内
        while(l < more){//l指针才是真正移动的指针,需要去判断的区域也是该指针到more的这个区间
            if(arr[l] < arr[r]){//判断是小于的元素,则需要将其与小于区间右侧元素交换
                swap(arr,++less,l++);
            }else if(arr[l] > arr[r]){
                swap(arr,l,--more);
            }else{
                l++;
            }
        }
        swap(arr,more,r);
        return new int[]{less,more};
    }

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

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        quickSort(arr3,0,arr3.length-1);
        for (int n: arr3) {
            System.out.print(n+" ");
        }
    }
}

堆排序

大根堆和小根堆概念:即根结点元素是堆里所有结点的最大者或者最小者。

堆的数据结构实际上就是一颗完全二叉树,并且其中任意一个结点的值总是不大于或不小于其父节点的值,所以要么就是大根堆,要么就是小根堆。

已知某个结点位置是index,那么它的父节点位置就是(index-1)/2,左孩子结点位置就是index*2+1,右孩子结点位置就是index * 2+2。

堆排序最核心的两个步骤就是向上调整和向下调整。下面是以大根堆为例。

向上调整即已知某个节点位置,比较该节点元素是否大于父节点元素,如果大则交换,并继续向上比较,否则退出。

向下调整即已知某个节点位置,比较左右孩子的大小,将大的孩子与该节点进行比较,如果孩子大于父亲则交换,并继续向下调整,否则退出。

堆排序则是将大根堆的首元素(最大元素)与最后一个元素进行交换,并将其踢出当前堆(heapSize–),再对首元素进行向下调整,凑成新的大根堆,循环该操作,直到heapSize为0,即整个区间有序。

public class HeapSort {

    public static 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 static void heapInsert(int[] arr,int index){
        while(arr[index] > arr[(index-1)/2]){
            swap(arr,index,(index-1)/2);
            index = (index-1)/2;
        }
    }

    //向下调整
    public static void heapify(int[] arr,int index,int heapSize){
        int left = index*2+1;
        while(left < heapSize){
            //找到左右孩子的最大值
            int lagest = left+1 < heapSize && arr[left] < arr[left+1] ? left+1 : left;
            //最大值和父节点进行比较
            lagest = arr[lagest] > arr[index] ? lagest : index;
            if(lagest == index){
                break;
            }
            swap(arr,lagest,index);
            index = lagest;
            left = index*2+1;
        }
    }

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

    public static void main(String[] args) {
        int[] arr1 = new int[]{1,5,3,2,7,9,8,4,6};
        int[] arr2 = new int[]{1,1,1,1,1,1,1,1,1};
        int[] arr3 = new int[]{9,8,7,6,5,4,3,2,1};
        int[] arr4 = new int[]{1,2,3,4,5,6,7,8,9};

        heapSort(arr4);
        for (int n: arr4) {
            System.out.print(n+" ");
        }
    }
}

根据代码,我们可以看出建堆的过程时间复杂度的O(N* logN)的,其实还有更快的方式,那就是从后往前进行heapify操作,为什么呢?因为任何一棵树的左右子树满足堆的要求,那么只需要将根结点向下调整,那么该树就一定满足堆结构,根据计算得出,这种方式的时间复杂度是O(N)的。

当然,也不难发现,叶子结点其实是无序进行向下调整的,所以可以直接从最后一个父节点开始。

//建堆的第二种方式
//寻找最后一个父节点,即heapSize-1(最后一个节点)的父节点
int index = (arr.length-2)/2;
for(;index >= 0;index--){
    heapify(arr,index,arr.length);
}

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值