排序算法小结-左神

排序算法

记录左神算法学习笔记之排序算法

1.冒泡排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YB2pChtt-1641298390746)(../pic/14979287-280a09dab967c74b.gif)]

    /**
     * 方法:冒泡排序
     * 复杂度; O(n^2)
     * 稳定性:稳定
     * 说明:遍历过程中把较大的值往后移动。
     */
    public static void bubbleSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        //0 ~ N-1 -> 0 ~ N - 2
        for (int i = arr.length - 1; i > 0; i--) {
            for (int j = 0; j < i; j++) {
                //左边元素大于右边,将大的交换到右边去
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                }
            }
        }
    }

2.选择排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gpsLU1WQ-1641298390747)(../pic/14979287-57867b59332cf182.gif)]

    /**
     * 方法:选择排序
     * 算法复杂度:O(N^2)
     * 稳定性:不稳定
     * 不稳定例子:  (7) 2 5 9 3 4 [7] 1.  (7)排到[7]后了
     * 每次查找最小的值放入到数组中
     */
    public static void selectSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        //每次选择最小的元素放在对应的位置上
        for (int i = 0; i < arr.length - 1; i++) {
            //在 i ~ n - 1位置查找最小值
            int minIndex = i;
            for (int j = i + 1; j < arr.length; j++) {
                //找到一个更小的元素,更新 minIndex
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            if (minIndex != i) swap(arr, minIndex, i);
        }
    }

3.插入排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GODoTmjd-1641298390748)(../pic/14979287-78142ffcc4722c0d.gif)]

    /**
     * 方法: 插入排序
     * 算法复杂度: O(N^2)
     * 稳定性:稳定
     * 1.每次保证 0 ~ i是已经排序好的
     * 2.每次从后往前查询插入的位置
     */
    public static void insertSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        //0~0 是有序的,想要 0~i是有序的
        for (int i = 1; i < arr.length; i++) {
            // j 是从 i - 1 开始的,就是已经排好序的最后一个位置开始的,然后和即将插入的元素相比,如果最后一个元素比
            //待插入的元素 arr[j + 1] 大,那需要将该元素往数组前移动
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
    }

4.归并排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQVcJJEL-1641298390748)(../pic/14979287-98b50b0755cf5028.gif)]

    /**
     * 方法:归并排序
     */
    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        process(arr, 0, arr.length - 1);
    }

    /**
     * 归并排序的递归过程
     * arr[L...R]范围上,变成有序的
     * 时间复杂度计算公式:master T(N) = a(N/b) + O(N^d)
     * 复杂度计算过程:子问题规模 N / 2,调用 2 次,除了递归之外的时间复杂度为 merge 的时间复杂度,为O(N)。a = 2,b = 2,d = 1满足master第一条 logb^a == d规则
     * T(N) = 2T(N/2) + O(N) => O(N*logN)
     * 时间复杂度为: O(nlogn)
     * 空间复杂度为:O(n)
     * 稳定性:稳定
     */
    public static void process(int[] arr, int l, int r) {
        if (l == r) return;
        int mid = l + ((r - l) >> 1);
        //让左边有序
        process(arr, l, mid);
        //让右边有序
        process(arr, mid + 1, r);
        //合并两个有序的数组
        merge(arr, l, mid, r);
    }

    /**
     * 合并两个有序数组
     */
    public static void merge(int[] arr, int l, int m, int r) {
        int[] temp = new int[r - l + 1];
        //定义左边数组指针 p1,右边数组指针p2,temp 数组指针p
        int p = 0, p1 = l, p2 = m + 1;
        while (p1 <= m && p2 <= r) {
            temp[p++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= m) {
            temp[p++] = arr[p1++];
        }
        while (p2 <= r) {
            temp[p++] = arr[p2++];
        }
        //将temp数组赋值到arr数组中
        if (temp.length >= 0) System.arraycopy(temp, 0, arr, l, temp.length);
    }
1.求小和问题?

在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[1, 3, 4, 2, 5]

1左边比1小的数:没有

3左边比3小的数:1

4左边比4小的数:13

2左边比2小的数为:1

5左边比5小的数为:1342

所以该数组的小和为:1+1+3+1+1+3+4+2 = 16

归并排序解决小和问题 (图解详细流程)

了解归并流程后,在merge过程中,产生小和。规则是左组比右组小,则产生小和。
>[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-21VmNo2m-1641298390748)(../pic/image-20211210105631775.png)]
注意

  • 当左组和右组数相等的时候,拷贝右组的数,不产生小和(区别于归并排序,我们是拷贝的左数组)

  • 当左组的数大于右组的时候,拷贝右组的数,不产生小和(因为此时如果拷贝左数组,就导致左组当前元素没有和右组的后面元素进行比较,会丢失情况,请特别注意)。

  • 实质是把找左边比本身小的数的问题,转化为找这个数右侧有多少个数比自己大,在每次merge的过程中,一个数如果处在左组中,那么只会去找右组中有多少个数比自己大
    >[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmvX5YZh-1641298390749)(../pic/image-20211210112144435.png)]

    /**
     * 左神:小和问题
     * 题目:在一个数组中,一个数左边比它小的数的总和,叫做小和,所有数的小和累加起来,叫做数组的小和。求数组的小和。例如[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 static int smallSum(int[] arr) {
        if(arr == null || arr.length < 2) return 0;
        return process1(arr,0,arr.length - 1);
    }

    public static int process1(int[] arr, int l, int r) {
        //只有一个数,不存在右组,小和为0
        if(l == r) return 0;
        int mid = l + ((r - l) >> 1);
        // 左侧merge的小和+右侧merge的小和+整体左右两侧的小和
        return process1(arr,l,mid) + process1(arr,mid + 1,r) + merge1(arr,l,mid,r);
    }

    /**
     * 归并排序的merge过程,在这个过程中记录小和
     */
    public static int merge1(int[] arr, int l, int m, int r) {
        int[] help = new int[r - l + 1];
        int p = 0, p1 = l, p2 = m + 1;
        int ans = 0;
        while (p1 <= m && p2 <= r){
            //当前的数是比右组小的,产生右组当前位置到右组右边界数量个小和,累加到 ans。否则 ans 加 0
            ans += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0;
            //归并数组,注意这里的区别,左边小拷贝左边,大于等于的选择拷贝右边
            help[p++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= m){
            help[p++] = arr[p1++];
        }
        while (p2 <= r){
            help[p++] = arr[p2++];
        }
        //将temp数组赋值到arr数组中
        if (help.length >= 0) System.arraycopy(help, 0, arr, l, help.length);
        return ans;
    }
2.求逆序对?

5.快排

1.荷兰国旗问题

荷兰国旗1.0版本:

Partion过程:

  • 给定一个数组arr,和一个整数 num。请把小于等于num的数放在数组的左边,大于num的数放在数组的右边(不要求有序)。要求额外空间复杂度为O(1),时间复杂度为O(N)。例如[5,3,7,2,3,4,1],num=3,把小于等于3的放在左边,大于3的放在右边
  • 设计一个小于等于区域,下标为 -1。
  • 开始遍历该数组,如果arr[i]<=num,当前数和区域下一个数交换,区域向右扩1,i++
  • arr[i] > num, 不做操作,i++

荷兰国旗问题:

  • 给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题。

  • 1.需要维护三个区域,左边的小于区域,中间的等于区域,右边的大于区域。用图中的0,1,2来表示。
    在这里插入图片描述

  • 2.初始化的时候红-白-蓝 三色是乱序的,所以此时的两条线我们是不是可以看成在最两侧
    在这里插入图片描述

  • 3.我们发现此时 C 等于0。是不是意味着,我们应把这个元素放到 A 的左侧,所以我们移动 A线。当然,我们也需要移动一下 C 的位置
    在这里插入图片描述

  • 新的 C 位置处等于2,那是不是说明这个元素应该位于 B 的右侧。所以我们要把该位置的元素 和 B位置处的元素进行交换,同时移动B。
    在这里插入图片描述

  • C 交换完毕后,C 不能向前移。因为C指向的元素可能是属于前部的,若此时 C 前进则会导致该位置不能被交换到前部。继续向下遍历。

  • 1)若遍历到的位置为0,则说明它一定位于A的左侧。于是就和A处的元素交换,同时向右移动A和C。

  • 2)若遍历到的位置为1,则说明它一定位于AB之间,满足规则,不需要动弹。只需向右移动C。

  • 3)若遍历到的位置为2,则说明它一定位于B的右侧。于是就和B处的元素交换,交换后只把B向左移动,C仍然指向原位置。(因为交换后的C可能是属于A之前的,所以C仍然指向原位置)

75. 颜色分类

    /**
     * 左神:荷兰国旗问题
     * 题目:给定一个数组,和一个整数num。请把小于num的数放在数组的左边,等于num的放中间,
     * 大于num的放右边。要求额外空间复杂度为O(1),时间复杂度为O(N)。[3,5,4,0,4,6,7,2],num=4。实质是经典荷兰国旗问题。
     */
    public static void sortArray(int[] arr, int target) {
        if (arr == null || arr.length < 2) return;
        int left = 0, right = arr.length - 1, cur = 0;
        while (cur <= right){
            if (arr[cur] < target){
                //交换到左区域,并且cur指针和left指针都移动
                swap2(arr,left,cur);
                cur++;
                left++;
            }else if (arr[cur] > target){
                //此时应该将元素移到右边界。右边界指针移动,cur指针也不动,因为我不知道当前移过来的元素是否满足条件,需要继续判断
                swap2(arr,cur,right);
                right--;
            }else {
                //当前元素与目标元素相等,只用移动cur指针
                cur++;
            }
        }
    }
2.快排

思想:借助荷兰问题,我们从数组中选择一个元素,然后让数组以该元素分为左右两部分。分完之后,该元素在排序数组中的位置一定是确定下来的。

继续计算左边数组和右边数组相同的过程,然后就能将找到所有元素的位置。注意:因为Partion过程会存在元素的交换(将相同的7交换到数组的最后方,所以快排是不稳定的。)

在最差情况下,如果我们每次找的元素都是位于位于排序后数组的末尾,则此时快排的时间复杂度会递增到 O ( N 2 ) O(N^2) O(N2) ,最好情况,其他各种情况的出现概率为 1 / N 1/N 1/N。对于这 N 种情况,数学上算出的时间复杂度最终期望是 O ( N l o g N ) O(NlogN) O(NlogN)

    /**
     * 左神快排:基于荷兰问题上进一步排序
     * 时间复杂度:O(NlogN)
     * 空间复杂度:O(logN)
     * 稳定性:不稳定
     */
    public static void quickSort(int[] arr) {
        quickSortHelper(arr, 0, arr.length - 1);
    }

    public static void quickSortHelper(int[] arr, int l, int r) {
        if (l < r) {
            //为了避免最坏的情况,我们随机算选择一个数作为划分值,将其放到数组的尾部,可以随机算去
//            swap2(arr,l + (int)(Math.random() * (r - l + 1)),r);
            //通常我们选中间元素就行
            int mid = l + (r - l) / 2;
            swap2(arr, mid, r);
            int[] p = partition(arr, l, r);
            quickSortHelper(arr, l, p[0] - 1);//小于区域
            quickSortHelper(arr, p[1] + 1, r);//大于区域
        }
    }

    /**
     * partition过程,这是一个处理 arr[l,r]的过程
     * 默认以 p = arr[r]做划分,分成三部分  <p  ==p  >p
     * 返回的是中间等于区域(左边界,右边界),所以这里反回了一个长度为2的数组res
     */
    public static int[] partition(int[] arr, int l, int r) {
        int less = l - 1;   // 小于 p 区域的边界
        int more = r;       // 大于 p 区域的边界
        while (l < more) {   // l表示当前操作的元素,而 arr[r] 为划分值
            if (arr[l] < arr[r]) {
                //当前元素小于划分值,应该放在左测区域,并且移动指针,因为less是前一个区域的边界
                swap2(arr, ++less, l++);
            } else if (arr[l] > arr[r]) {
                //当前元素大于划分值,要分到右边界,此时交换元素,并且l指针不能动,因为我不确定当前交换的值是否满足条件
                swap2(arr, --more, l);
            } else {
                //当前元素为划分值,则只用移动l指针
                l++;
            }
        }
        //将划分值放回右边区域的边界位置
        swap2(arr, more, r);
        //返回中间等于区域的左右边界
        return new int[]{less + 1, more};
    }

6.堆排序

1.大根堆和小根堆

在这里插入图片描述

基础性质:

  • 其中 i i i 表示数组中的索引,将它对应成堆后的性子。

  • 父节点索引: ( i − 1 ) / 2 (i -1)/2 (i1)/2

  • 左孩子索引:$ 2*i + 1$

  • 右孩子索引: 2 ∗ i + 2 2*i + 2 2i+2

  • 大根堆: a r r [ i ] > a r r [ 2 ∗ i + 1 ] & & a r r [ i ] > a r r [ 2 ∗ i + 2 ] arr[i] > arr[2*i + 1] \&\& arr[i] > arr[2*i+2] arr[i]>arr[2i+1]&&arr[i]>arr[2i+2]

  • 小根堆: a r r [ i ] < a r r [ 2 ∗ i + 1 ] & & a r r [ i ] < a r r [ 2 ∗ i + 2 ] arr[i] < arr[2*i + 1] \&\& arr[i] < arr[2*i+2] arr[i]<arr[2i+1]&&arr[i]<arr[2i+2]

  • 完全二叉树中如果每棵子树的最大值都在顶部就是大根堆

  • 完全二叉树中如果每颗子树的最小值都在顶部就是小根堆

两个重要堆结构操作:

  • heapInsert

要构建一个大根堆,把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式 p a r e n t = ( i − 1 ) / 2 parent = (i-1) / 2 parent=(i1)/2找到父节点去比较,如果比父节点大就和父节点交换向上移动,移动后再用自己当前位置和父亲节点比较…,小于等于父节点不做处理。这样用户每加一个数,我们都能保证该结构是大根堆。

我们的调整代价实际上就是这颗树的高度层数, l o g N logN logN

  • heapify

删除了最大值,也就是 a r r [ 0 ] arr[0] arr[0]位置,之后我们把堆最末尾的位置调整到 a r r [ 0 ] arr[0] arr[0]位置,堆大小减一。让现在 a r r [ 0 ] arr[0] arr[0]位置的数找左右孩子比较…,进行hearify操作,让其沉下去。沉到合适的位置之后,仍然是大根堆。

heapify的下沉操作,仍然是树的高度,logN

整个过程如下:

在这里插入图片描述

2.手写大堆:
/**
 * 创建大根堆
 */
public class MaxHeap {

    /**
     * 存储堆元素数组
     */
    private int[] heap;

    /**
     * 堆的大小限制,当然超过了会进行数组的两倍扩容
     */
    private final int capacity;

    /**
     * 表示目前这个堆收集了多少个数,也表示添加的下一个数应该放在哪个位置
     */
    private int heapSize;

    public MaxHeap(int capacity) {
        this.capacity = capacity;
        heap = new int[capacity];
        heapSize = 0;
    }

    public boolean isEmpty() {
        return heapSize == 0;
    }

    public boolean isFull() {
        return heapSize == capacity;
    }

    public void push(int value){
        if (heapSize == capacity){
            throw new RuntimeException("heap is full");
        }
        heap[heapSize] = value;
        //调整堆结构,heapSize作为操作数组的指针
        heapInsert(heap,heapSize++);
    }

    /**
     * 从堆中取出堆顶元素,即返回最大值,并且在大根堆中,
     * 把最大值删掉剩下的数,依然保持大根堆组织
     */
    public int pop(){
        if (heapSize == 0) {
            throw new RuntimeException("heap is empty");
        }
        int ans = heap[0];
        //这里的移除只是改变 heapSize 指针。至于数组的元素还存在对我们堆结构没有任何影响。因为我们在push操作中会覆盖当前值
        //我们将堆中最后一个元素移动到堆顶执行heapify操作,下沉堆顶元素
        swap(heap,0,--heapSize);
        //执行下沉操作
        heapify(heap,0,heapSize);
        return ans;
    }
    
    public int peek(){
        if (heapSize == 0) {
            throw new RuntimeException("heap is empty");
        }
        return heap[0];
    }

    /**
     * 向堆中插入元素
     */
    private void heapInsert(int[] arr,int index){
        //arr[index] 不比 arr[index父]大了,停止heapInsert过程
        //注意当比较到 0 位置即堆顶时候,此时循环也不会进入了
        while (arr[index] > arr[(index - 1) / 2]){
            swap(arr,index,(index - 1) / 2);
            //将指针移动到父节点,继续上升的过程
            index = (index - 1) / 2;
        }
    }

    /**
     * 从index位置,往下看,不断的下沉,停的条件:
     * 我的孩子都不再比我大;已经没孩子了
     */
    private void heapify(int[] arr,int index,int heapSize){
        //找到左孩子索引
        int leftIndex = index * 2 + 1;
        //这里如果左孩子越界,那右孩子更越界,右孩子等于左孩子+1
        while (leftIndex < heapSize){
            //下沉操作,父节点与左右孩子相比, 左右两个孩子中,谁大,谁把自己的下标给largest,因为大的节点得上升
            // 选择右  ->  (1) 有右孩子   && (2)右孩子的值比左孩子大才行
            int largest = leftIndex + 1 < heapSize && arr[leftIndex + 1] > arr[leftIndex] ?  leftIndex + 1 : leftIndex;
            //左右孩子中最大值,和当前值父节点比较,谁大谁把下标给largest(当前,左,右的最大值下标)
            largest = arr[largest] > arr[index] ? largest : index;
            //index 位置上的数比左右孩子的数都大,已经无需下沉
            if(index == largest) break;
            //交换父节点和子节点中的较大孩子。周而复始进行
            swap(arr,index,largest);
            //更新父节点的index,即父节点往下沉
            index = largest;
            //继续看它的左右孩子是否需要调整
            leftIndex = index * 2 + 1;
        }
    }
    
    private void swap(int[] arr,int i,int j){
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

3.堆排序

   /**
     * 左神:堆排序
     * 时间复杂度为:O(NlogN)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * 对于用户给的所有数据,我们先让其构建成为大根堆
     * 对于0到N-1位置的数,我们依次让N-1位置的数和0位置的数(全局最大值)交换,此时全局最大值来到了数组最大位置,堆大小减一,
     * 再heapify调整成大根堆。再用N-2位置的数和调整后的0位置的数交换,相同操作。直至0位置和0位置交换。每次heapify为logN,交换调整了N次
     * <p>
     * 堆排序额为空间复杂度为O(1),且不存在递归行为
     */
    public static void heapSort(int[] arr) {
        if (arr == null || arr.length < 2) return;
        //1.将元素数组构建成堆事件复杂度为 0(NlogN)
        for (int i = 0; i < arr.length; i++) {
            heapInsert(arr, i);
        }
        //1.1拓展,如果直接给定一个数组,我们直接执行heapify操作构建大堆,从末尾开始看是否需要heapify
//        for (int i = arr.length - 1; i >= 0;i--){
//            heapify(arr,i,arr.length);
//        }

        //2.将堆顶元素移到末尾,堆的heapSize--;
        int heapSize = arr.length;
        swap2(arr, 0, --heapSize);
        //开始下沉操作
        while (heapSize > 0) {
            heapify(arr, 0, heapSize);
            swap2(arr, 0, --heapSize);
        }
    }

    /**
     * 向堆中插入元素,维持大堆结构
     * arr[index]刚来的数,通过比较进行上升
     */
    public static void heapInsert(int[] arr, int index) {
        //与父节点相比,大于就上身,否则停止
        while (arr[index] > arr[(index - 1) / 2]) {
            swap2(arr, index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    /**
     * 下沉操作,将当前 index 位置下沉到合适的位置,保持堆结构稳定
     */
    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        while (left < heapSize) {
            //1.找左右孩子中较大的一个
            int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
            //2.将孩子与父节点进行比较,父节点比孩子小,则需要下沉
            largest = arr[index] < arr[largest] ? largest : index;
            //3.如果父节点的索引没变,则说明下层已经满足大堆结构了,直接break
            if (largest == index) break;
            //4.否则父节点与子节点交换
            swap2(arr, index, largest);
            //5.父节点指针移动,移动到孩子节点,接续判断孩子节点的树是否满足大堆结构,周而复始
            index = largest;
            left = index * 2 + 1;
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值