排序算法小结-左神

排序算法小结-左神,第1张

排序算法小结-左神 排序算法

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

1.冒泡排序

    
    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.选择排序

    
    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.插入排序

    
    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.归并排序

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

    
    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小的数:1、3

2左边比2小的数为:1

5左边比5小的数为:1、3、4、2

所以该数组的小和为: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)]" />

    
    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);
    }

    
    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之间,满足规则,不需要动d。只需向右移动C。

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

75. 颜色分类

    
    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)

    
    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);//大于区域
        }
    }

    
    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 (i−1)/2

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

  • 右孩子索引: 2 ∗ i + 2 2*i + 2 2∗i+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[2∗i+1]&&arr[i]>arr[2∗i+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]

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

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

两个重要堆结构 *** 作:

  • heapInsert

要构建一个大根堆,把所有的数依次添加到一个数组(下标从0开始)中去,每次添加一个数的时候,要去用找父亲节点的公式 p a r e n t = ( i − 1 ) / 2 parent = (i-1) / 2 parent=(i−1)/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;
        }
    }

    
    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.堆排序

   
    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);
        }
    }

    
    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;
        }
    }

    
    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;
        }
    }

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5697177.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存