【算法】排序和二分以及相关刷题(学习篇)

在这里插入图片描述

排序

1. 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
2. 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
1. 选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

时间复杂度 O(N^2)

function selectionSort(arr) {
    varlen = arr.length;
    varminIndex, temp;
    for(vari = 0; i < len - 1; i++) {
        minIndex = i;
        for(varj = i + 1; j < len; j++) {
            // 寻找最小的数
            if(arr[j] < arr[minIndex]) {  
                // 将最小数的索引保存
                minIndex = j;                
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    returnarr;
} 
2. 插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

时间复杂度 O(N^2)

function insertionSort(arr) {
    varlen = arr.length;
    varpreIndex, current;
    for(vari = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    returnarr;
}
3. 冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

时间复杂度 O(N^2)

function bubbleSort(arr) {
    varlen = arr.length;
    for(vari = 0; i < len - 1; i++) {
        for(varj = 0; j < len - 1 - i; j++) {
            // 相邻元素两两对比
            if(arr[j] > arr[j+1]) {  
                // 元素交换
                vartemp = arr[j+1];     
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    returnarr;
}
4. 堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

时间复杂度 O(NlogN)

class Solution {
    public int[] sortArray(int[] nums) {
        PriorityQueue<Integer> q = new PriorityQueue<>(new Comparator<Integer>() {
			@Override
			public int compare(Integer o1, Integer o2) {
			    // 小根堆
				return o1-o2;
			}
        });
        int[] ans = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            q.add(nums[i]);
        }
        int index = 0;
        while (!q.isEmpty()) {
            ans[index] = q.poll();
            index++;
        }
        return ans;
    }
}
5. 快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

时间复杂度 O(NlogN)

class Solution {
    public int[] sortArray(int[] nums) {
        //快排
        this.quickSort(nums,0,nums.length - 1);
        return nums;
    }

    public void quickSort(int[] nums,int k, int left, int right) {
        if (left >= right) return;
        // 交换数据
        int mid = sort(nums,left,right);
        // 排序左边
        quickSort(nums,k,left,mid);
        // 排序右边
        quickSort(nums,k,mid + 1,right);
    }
    // 随机选一个数S,根据这个数把小于S的放左边,大于S放右边
    public int sort(int[] nums, int left, int right){
        // 防止出现选择的数都是最小或最大,每次取一个随机数
        int mid = left + (int)(Math.random() * (right - left));
        int l = left;
        int r = right;
        // 选择的位置的值
        int val = nums[mid];
        while (l <= r) {
            // 小于选择的值,直接l向右移动
            while (nums[l] < val) l++;
            // 大于选择的值,直接r向左移动
            while (nums[r] > val) r--;
            if(r == l) break;
            // 符合区间条件直接交换左右的值
            if (l <= r){
                int swap = nums[r]; 
                nums[r] = nums[l]; 
                nums[l] = swap;
                l++;
                r--; 
            }
        }
        // 最后返回最后left和right相等中间的元素的下标
        return r;
    }
}
6. 归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

时间复杂度 O(NlogN)

class Solution {
    public int[] sortArray(int[] nums) {
        this.sort(nums,0,nums.length - 1);
        return nums;
    }
    // 归并排序
    public void sort(int[] nums,int left, int right) {
        // 终止条件
        if (left >= right) return;
        int mid = (left + right) >> 1;
        // 左边
        sort(nums,left,mid);
        // 右边
        sort(nums,mid + 1,right);
        // 合并
        merge(nums,left,mid,right);
    }

    public void merge(int[] nums, int left, int mid, int right) {
        // 数组长度
        int length = right - left + 1;
        int[] ans = new int[length];
        int l = left;
        int r = mid + 1;
        int index = 0;
        // 归并排序进行 双数组合并
        for (int i = 0; i < ans.length; i++ ) {
            // 当l等于mid+1 或者 (l<=mid  && r<=right && nums[l] > nums[r]) 都是 r++
            // 每次只用考虑一个方向的判断,其他进入elese
            if(l > mid || (r <= right && nums[l] > nums[r])){
                ans[i] = nums[r++];
            }else{
                // 其他的
                ans[i] = nums[l++];
            }
        }
        // 拷贝临时数组到原数组
        for (int i = 0; i < ans.length; i++ ) {
            nums[left + i] = ans[i];
        }
    }
}
7. 计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数,并且这个范围不是太大,如果范围超过一定的长度,那么res数组就会很大,内存开销很大。
计数排序对于元素值在正整数区间是可行的,对于负数还需要另外的考虑

时间复杂度 O(M + N) N为元素个数 , M为元素值区间

class Solution {
    public int[] sortArray(int[] nums) {
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < nums.length; i++) {
            max = Math.max(nums[i],max);
        }
        int[] res = new int[max + 1];
        for (int i = 0; i < nums.length; i++) {
            res[nums[i]]++;
        }
        int index = 0;
        for (int i = 0; i < res.length; i++) {
            while(res[i]-->0)nums[index++] = i;
        }
        return nums;
    }
}
  1. 寻找旋转排序数组中的最小值 II
class Solution {
    public int findMin(int[] nums) {
        int left = 0,right = nums.length - 1;
        while (left < right){
            int mid = (left + right) >> 1;
            // 如果mid == right 则最小值肯定在mid和right之间
            // 则right  = right - 1;右节点向做移动一个数
            if( nums[mid] == nums[right]){
                right = right - 1;
            }else if (nums[mid] < nums[right]){
                right = mid ;
            }else{
                left = mid + 1;
            }
        }
         System.out.println(left + ";" + right);
        return nums[left];
    }
}
  1. 寻找峰值
/* 思维逻辑
1. 三分查询-lmid 和 rmid 
    - 如果lmid < rmid 即峰值肯定在lmid的右侧 即区间 [lmid , r]
    - 如果lmid > rmid 单调递减了,那么峰值肯定在左侧,即[l,rmid]
2. 尽可能的缩短lmid和rmid的值 
3. 本题的数组元素为int,那么可以lmid = (left + right)>>1;rmid就可以为 lmid + 1;
4. 如果数组元素为double类型那么lmid = (left + right)>>1;rmid就可以为 lmid + 0.01;
*/
class Solution {
    public int findPeakElement(int[] nums) {
        if (nums.length == 1) {}
        int left = 0, right = nums.length - 1;
        while (left < right){
            int lmid = (left + right) >> 1;
            int rmid = lmid + 1;
            // lmid < rmid 即单调递增 取区间为 [lmid + 1,right]
            if (nums[lmid] <= nums[rmid]){
                left = lmid + 1;
            }else{
            // lmid > rmid 即单调递减 取区间为 [l,rmid - 1]
                right = rmid - 1;
            }
            // System.out.println(mid +";"+left);
        }
        return left;
    }
}
  1. 分割数组的最大值

/* 思维逻辑
1. 提取题目信息:
    - 一个非负整数数组 nums 和一个整数 m
    - 将这个数组分成 m 个非空的连续子数组
    - 设计一个算法使得这 m 个子数组各自和的最大值最小
2. 把nums数组分成m个连续子数组,并且求出所有子数组的和中最大值
3. 然后有很多种分成连续子数组的方式,求其中最大值最小的一次分法
4. 那么猜想一个数S满足此前的条件
5. 直接贪心算法可以分成多少个组,只要小于等于当前的子数组m个数即可符合要求
6. 此题解就转变为寻找第一个满足条件的数S
7. 二分的区间[数组最大值,数组的总和]
8. 从这个区间寻找到第一个满足条件的数S
*/
class Solution {
    public int splitArray(int[] nums, int m) {
        // 二分的最大值查找
        int sum = 0;
        int left = 0;
        for (int i = 0; i < nums.length;i++) {
            sum += nums[i];
            // 起始位置为数组中最大的值
            left = Math.max(left, nums[i]);
        }
        int right = sum;
        // 二分猜想一个数
        while (left < right) {
            int mid = (left + right) >> 1;
            if (isFind(nums,m,mid)) {
                right = mid;
            }else {
                left = mid + 1;
            }
        }
        return left;
    }
    // 假设一个数S,这个数S是各个数组最大和中最小的值
    // 那么 每个子数组的和都小于S
    public boolean isFind(int[] nums, int m, int S) {
        // 初始为1
        int count = 1;
        int sum = 0;
        // 贪心的把数组的元素相加
        for (int i = 0; i < nums.length; i++) {
            // 如果当前元素+sum大于等于S,即应该分割新数组,并且数组数count++
            if (sum + nums[i] <= S) {
               sum += nums[i];
            }else{
                 // i元素为下一个数组的头元素,sum为下一个数组的初始值
                sum = nums[i];
                count++;
            }
        }
        // 计算分割的数组count < m时:[7,2,5,10,8]  m=2
        // ex : S为33
        // 那么count=1,那么分一个数组就满足条件,把这个数组分成两个数组也能满足条件
        return count <= m;
    }
}
  1. 数组的相对排序
class Solution {
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        return this.sortArray(arr1,arr2);
    }
    // 计数
    public int[] sortArray(int[] nums,int[] arr2) {
        int max = 1000;
        int[] res = new int[max + 1];
        for (int i = 0; i < nums.length; i++) {
            res[nums[i]]++;
        }
        int index = 0;
        int[] ans = new int[nums.length];
        for (int i = 0; i < arr2.length; i++) {
            while(res[arr2[i]]-->0) ans[index++] = arr2[i];
        }
        for (int i = 0; i <= max; i++) {
            while(res[i]-->0)ans[index++] = i;
        }
        return ans;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值