左神算法整理笔记03

一个排序的重要性不仅仅局限于他的功能,思路的重要性
重要的排序算法:归并排序、快排、堆排

  • 归并排序的思想:分治思想和master时间复杂度公式,合的思想
  • 快排的思路:分的思想
  • 堆排的思想:堆结构的思想

归并的应用-小和

在这里插入图片描述

public static int mergeSort(int[] arr, int left, int right){
    if (left == right) return 0;
    int mid = left + ((right-left)>>1);
    // 小和等于左侧的小和+右侧的小和+合并过程中的小和
    return mergeSort(arr,left,mid)
            +mergeSort(arr,mid+1,right)
            +merge(arr,left,mid,right);
}

public static int merge(int[] arr, int left, int mid, int right) {
    int[] help = new int[right-left+1];
    int i = 0;
    int p1 = left;
    int p2 = mid+1;
    int res = 0;
    while (p1 <= mid && p2 <= right) {
    	// 计算和:只有这里的逻辑和归并不一样,其他都一样
        res += arr[p1] < arr[p2] ? (right-p2+1) * arr[p1] : 0;
        help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
    }
    while (p1 <= mid) {
        help[i++] = arr[left++];
    }
    while (p2 <= right) {
        help[i++] = arr[p2++];
    }
    for (int j = 0; j < help.length; j++) {
        arr[left+j] = help[j];
    }
    return res;
}

解析计算和步骤

  • 134和25合并的时候找和
  • 1比2小,那么就有2个1
  • 3比2大,比5小,那么就只有1个3
  • 4比2大,比5小,那么就只有1个4
  • 2 * 1+1 * 3+1 * 4=9
    在这里插入图片描述

逆序对变形

在这里插入图片描述

class Solution {

    // 归并排序的应用-分治思想
    public int reversePairs(int[] nums) {
        if (nums == null || nums.length <2) return 0;
        return mergeSort(nums, 0, nums.length-1);
    }

    public int mergeSort(int[] nums, int left, int right) {
        if (left == right) return 0;
        int mid = left + ((right-left)>>1);
        return mergeSort(nums, left, mid)
                + mergeSort(nums, mid+1, right)
                + merge(nums, left, mid, right);
    }
    public int merge(int[] nums, int left, int mid, int right) {
        int[] help = new int[right-left+1];
        int i = 0;
        int p1 = left;
        int p2 = mid+1;
        int res = 0;
        while (p1 <= mid && p2 <= right) {
            // 主要是这里的区别:如果是求所有的和则要写出(right-p2+1)*nums[p1]
            res += nums[p1] > nums[p2] ? (right-p2+1) * 1 : 0;
            help[i++] = nums[p1] > nums[p2] ? nums[p1++] : nums[p2++];
        }
        while (p1 <= mid) {
            help[i++] = nums[p1++];
        }
        while (p2 <= right) {
            help[i++] = nums[p2++];
        }
        for (int k = 0; k < help.length; k++) {
            nums[left+k] = help[k];
        }
        return res;
    }
}

排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗

// 归并排序
public ListNode sortList(ListNode head) {
    return head == null ? null : mergeSort(head);
}

private ListNode mergeSort(ListNode head) {
    if (head.next == null) {
        return head;
    }
    // 利用快慢指针将链表分成两段
    ListNode p = head, q = head, pre = null;
    while (q != null && q.next != null) {
        pre = p;
        p = p.next;
        q = q.next.next;
    }

    pre.next = null;

    ListNode l = mergeSort(head);
    ListNode r = mergeSort(p);
    return merge(l, r);
}

ListNode merge(ListNode l, ListNode r) {
    ListNode dummyHead = new ListNode(0);
    ListNode cur = dummyHead;
    while (l != null && r != null) {
        if (l.val <= r.val) {
            cur.next = l;
            cur = cur.next;
            l = l.next;
        } else {
            cur.next = r;
            cur = cur.next;
            r = r.next;
        }
    }
    if (l != null) {
        cur.next = l;
    }
    if (r != null) {
        cur.next = r;
    }
    return dummyHead.next;
}

快排

快排的引例-荷兰国旗问题

在这里插入图片描述

  • 思想
    在这里插入图片描述
  • 代码
public static int[] partition(int[] nums, int left, int right, int targrt) {
    int less = left - 1;
    int more = right + 1;
    int index = left;
    while (index < more) {
        if (nums[index] < targrt) {
            // 交换过来的数已经比较过了,直接可以++
            swap(nums, ++less, index++);
        } else if (nums[index] > targrt) {
            // 这里cur不加,是因为交换过来这个数,还没有比较多,需要再比较一次
            swap(nums, --more,index);
        } else {
            index++;
        }
    }
    // 返回左右边界
    return new int[] {less+1, more-1};
}
public static void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

经典快排

public static void quickSort(int[] nums, int left, int right) {
    if (left < right) {
        // 荷兰国旗划分好之后,继续递归划分剩下两个区域
        int[] p = partition(nums, left, right);
        quickSort(nums, left, p[0]-1);
        quickSort(nums, p[1]+1, right);
    }
}
// 默认以最后一个数做划分
public static int[] partition(int[] nums, int left, int right) {
    int less = left - 1;
    int more = right+1;
    int target = nums[right]
    while (left < more) {
        if (nums[left] < target) {
            // 交换过来的数已经比较过了,直接可以++
            swap(nums, ++less, left++);
        } else if (nums[left] > target) {
            // 这里cur不加,是因为交换过来这个数,还没有比较多,需要再比较一次
            swap(nums, --more,left);
        } else {
            left++;
        }
    } 
    // 因为最后一个位置没有参与遍历,这里是归位(边界处理的炫技),加target就可以去掉这个步骤
    // swap(nums, more, right);
    // 返回等于区域的范围
    return new int[] {less+1, more-1};
}

随机快排

  • 经典快排缺点:和数据有关系,经典快排总拿最后一个数(某个固定的数),会出现退化的情况
  • 例如:1234567这个的时间复杂度就会退化成(O(N^2))
  • 随机快排的时间复杂度是期望时间复杂度(O(N*logN))
  • 随机快排是最常用,最重要的排序,工程上的排序都是用随机快排实现的,工程上不允许出现递归,所以工程上的快排是迭代版的快排(之后需要掌握快排怎么转换非递归版本)
public static void quickSort(int[] nums, int left, int right) {
    if (left < right) {
    	// 随机选一个数和最后一个数交换
    	swap(nums,left+(int)(Math.random()*(right-left+1)),right);
        // 荷兰国旗划分好之后,继续递归划分剩下两个区域
        int[] p = partition(nums, left, right);
        quickSort(nums, left, p[0]-1);
        quickSort(nums, p[1]+1, right);
    }
}

应用:绕过原始数据状况

  • 随机函数
  • hash函数

堆排

  • 堆结构非常重要
    在这里插入图片描述
  • 堆其实是完全二叉树在这里插入图片描述
    在这里插入图片描述
    在完全二叉树的基础上,对其做一些处理就可以得到大、小根堆结构
  • 大根堆任何一颗子树的最大值都是头部
  • 小根堆任何一颗子树的最小值都是头部
  • 对完全二叉树做处理
    完全二叉树可以基于数组实现(在逻辑上实现的,实际结构还是数组)

左孩子:2*i + 1
右孩子:2*i + 2
父节点:(i - 1)/2(重要,基于这个实现完全二叉树到堆结构改造)
在这里插入图片描述

  • 实现大根堆改造
    在这里插入图片描述
  1. 2-1-3中不是大根堆,让3根据(i-1/2)计算得到父节点进行交换,此时得到0-2上的部分大根堆
  2. 同理6插入进来,进行交换,得到0-3上的大根堆
  3. 同理使得整个数组编程大根堆
// 交换
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;
    }
}
// 得到0-i之间的大根堆
for (int i = 0; i < arr.length; i++) {
    heapInsert(arr, i);
}
  • 建立大根堆的时间复杂度
    需要比较的个数就是二叉树的高度(完全二叉树的高度是logN),所以的加起来(log1+log2+log3+…logn-1=O(n)),所以建立大根堆时间复杂度o(n)
  • 大根堆的另一个操作(heapify):用于解决大根堆中的某一个数发生变化,使得大根堆变成非大根堆,进行调整变回大根堆的操作
public static void heapify(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;
    while (left < heapSize) {
    	// 求左右孩子最大值
        int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left+1 : left;
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) {
            break;
        }
        swap(arr, largest, index);
        // 还是得到交换这个的位置的左孩子
        index = largest;
        left = index * 2 + 1;
    }
}

堆结构的重要性:

  • 堆结构的重要性:贪心问题的结构几乎都是堆结构搞定的,堆又被称为优先级队列
  • 堆结构的调整复杂度只和层数有关系,也就是调整一次(加一个数或者减一个数)的时间复杂度是Log(n)
  • 来感受一下log的时间复杂度的魅力:log(400,0000,0000)=10.6(400亿的时间复杂度大概是10)
    堆排序
  • 理解了堆结构和堆的几个操作,堆排序就比较简单
  • 首先是将大根堆和最后一个数交换,此时最大的数在最后(这个数已排好序)
  • 利用headify(下沉操作),使得0-n-2的数重新变成大根堆
  • 重复步骤2操作,将大根堆和倒数第二个数交换(那个后两个数也将拍好序)
  • 重复步骤
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 largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left+1 : left;
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) {
            break;
        }
        swap(arr, largest, index);
        index = largest;
        left = index * 2 + 1;
    }
}

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

堆排的应用

如果需要随时接收数据,并且需要随时读取所有数据的中位数,则利用堆进行运算

算法流程:
1.首先创建两个堆,一个大根堆,一个小根堆
2.将第一个数据防入大根堆,第二个数据到来之后跟大根堆中的数据进行比较,如果数据小于大根堆中的根,则放入大根堆中,此时大根堆中会有两个数据,而小根堆中没有数据。
3.如果出现大根堆和小根堆中的数目不相等,相差2,则将数据较多的堆的父节点弹出,放入数据较少的数据堆中当父节点。堆顶需要删掉则可以执行将堆底最后的数据跟堆顶换位,将堆的数据heapsize减少,之后再进行heapify,则重新调整为大根堆。

经过上述三个步骤,则可以实现大根堆中的数据小于小根堆中的数据,中位数的数据只会从大根堆和小根堆的父节点中选出,随时进入数据,则可以随时调整,复杂度较低。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值