1.归并排序
将数组分成两部分,分别进行排序,后归并起来。涉及递归思想。需要辅助空间。
时间复杂度O(nlog(n)),不像冒泡、选择浪费了大量比较。
mergeSort函数中不停地递归拆分数组
merge函数中用双指针合并两数组
import java.util.*;
public class Solution {
public int[] MySort (int[] arr) {
int n = arr.length;
int[] tmp = new int[n];
if(n>1) mergeSort(arr,tmp,0,n-1);
return arr;
}
public void mergeSort(int[] arr, int[] tmp, int left, int right){
if(left == right) return;
if(left < right){
int mid = (left+right) >> 1;
mergeSort(arr, tmp, left, mid);
mergeSort(arr,tmp,mid+1, right);
merge(arr, tmp,left,mid,right);
}
}
public void merge(int[] arr, int[] tmp, int left, int m, int right){
int i = left;
int tmpIndex = left;
int j = m+1;
while(i<=m && j<=right){
tmp[tmpIndex++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while(i<=m){
tmp[tmpIndex++] = arr[i++];
}
while(j<=right){
tmp[tmpIndex++] = arr[j++];
}
// 注意:排好序后一定要放回arr数组
for (i = left; i <= right; i++) {
arr[i] = tmp[i];
}
}
}
归并分治
计算数组的小和_牛客题霸_牛客网
思路:左部分小和+右部分小和+跨左右的小和
在计算跨左右小和的时候,如果左、右部分各自有序,能获得一些计算上的遍历,所以在求解过程中加入归并排序的过程。
import java.util.Scanner;
import java.io.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static int n;
public static int[] arr;
public static int[] tmp;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
// 注意 hasNext 和 hasNextLine 的区别
while (in.nextToken() != StreamTokenizer.TT_EOF) { // 注意 while 处理多个 case
int n = (int)in.nval;
arr = new int[n];
tmp = new int[n];
for(int i=0; i<n; i++){
in.nextToken();
arr[i] = (int)in.nval;
}
out.println(smallSum(0,n-1));
}
out.flush();
out.close();
}
public static long smallSum(int l, int r){
if(l==r) return 0;
int m=(l+r) >> 1;
return smallSum(l,m)+smallSum(m+1,r)+merge(l,m,r);
}
public static long merge(int l, int m, int r){
long ans = 0;
for(int j=m+1,i=l,sum=0; j<=r; j++){
while(i<=m && arr[i]<=arr[j]){
sum += arr[i++];
}
ans += sum;
}
int i=l;
int j=m+1;
int tmpIndex=l;
while(i<=m && j<=r){
tmp[tmpIndex++] = arr[i]<=arr[j] ? arr[i++] :arr[j++];
}
while(i<=m){
tmp[tmpIndex++] = arr[i++];
}
while(j<=r){
tmp[tmpIndex++] = arr[j++];
}
for(i=l; i<=r; i++){
arr[i] = tmp[i];
}
return ans;
}
}
493.翻转对 - 力扣(LeetCode)
与上一题思路一样
class Solution {
public int reversePairs(int[] arr) {
int n=arr.length;
int[] tmp = new int[n];
return count(arr,tmp,0,n-1);
}
public int count(int[] arr, int[] tmp, int l, int r){
if(l==r) return 0;
int m = (l+r) >>1;
return count(arr, tmp, l, m)+count(arr,tmp, m+1, r)+merge(arr, tmp, l,m,r);
}
public int merge(int[] arr, int[] tmp, int l, int m, int r){
int ans = 0;
for(int i=l,j=m+1; i<=m; i++){
while(j<=r && (long)arr[i] > (long)arr[j]*2){
j++;
}
ans += j-m-1;
}
int i=l;
int tmpIndex=l;
int j=m+1;
while(i<=m && j<=r){
tmp[tmpIndex++] = arr[i]<=arr[j] ? arr[i++] : arr[j++];
}
while(i<=m){
tmp[tmpIndex++] = arr[i++];
}
while(j<=r){
tmp[tmpIndex++] = arr[j++];
}
for(i=l; i<=r; i++){
arr[i] = tmp[i];
}
return ans;
}
}
2.快速排序
随机找一个分界点p
,通过交换元素使得nums[l...p-1]
都小于等于nums[p]
,且nums[p+1..h]
都大于nums[p]
,然后在左、右部分递归调用。
优化:荷兰国旗问题
- 已知arr[l....r]范围上一定有x这个值
- 划分数组 <x放左边,==x放中间,>x放右边
- 把全局变量first, last,更新成==x区域的左右边界
- <x,first与i交换,i++,first++
- ==x,i++
- >x,last与i交换,last--,i不变
- 当last<i时,返回first、last,这个区间就是==x的情况,然后再<x、>x两个范围重复。
优点:一次搞定==x的所有值
class Solution {
public int[] sortArray(int[] nums) {
quickSort2(nums,0,nums.length-1);
return nums;
}
// 荷兰国旗问题
public static int first, last;
public static void quickSort2(int[] arr, int l, int r) {
if(l>=r) return;
// 随机这一下,常数时间比较大
// 但只有这一下随机,才能在概率上把快速排序的时间复杂度收敛到O(n * logn)
int x = arr[l + (int) (Math.random() * (r - l + 1))];
partition2(arr, l, r, x);
// 为了防止底层的递归过程覆盖全局变量
// 这里用临时变量记录first、last
int left = first;
int right = last;
quickSort2(arr, l, left - 1);
quickSort2(arr, right + 1, r);
}
// 已知arr[l....r]范围上一定有x这个值
// 划分数组 <x放左边,==x放中间,>x放右边
// 把全局变量first, last,更新成==x区域的左右边界
public static void partition2(int[] arr, int l, int r, int x) {
first = l;
last = r;
int i = l;
while (i <= last) {
if (arr[i] == x) {
i++;
} else if (arr[i] < x) {
swap(arr, first++, i++);
} else {
swap(arr, i, last--);
}
}
}
public static void swap(int[] nums, int i, int j){
if(i==j) return;
nums[i] ^= nums[j];
nums[j] ^= nums[i];
nums[i] ^= nums[j];
}
}
快速排序核心思想就是每次在当前区间 [left, right] 中选择出一个元素 nums[p],然后将区间内所有大于它的元素和所有小于它的元素都放到其两侧【具体放在哪一侧取决于是升序还是降序】,然后再递归去处理两侧区间。
双指针交换的方式划分左右区间
215.数组中第K个最大元素.
将大于切分值的元素都放到左侧,小于切分值得元素都放到右侧。因此我们可以使用双指针分别从两端往中间搜索,分别找到左侧小于切分值的元素和右侧大于切分值的元素,交换之,直到两个指针相遇。
对于这道题,我们可以选择区间的中间值作为切分元素,并且我们要事先将切分值交换到区间的右边界:
- 避免在划分左右区间的时候,将切分值给覆盖掉。
- 最终才可以将切分值放到正确的位置。
class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSortKth(nums,k,0,nums.length-1);
}
private int quickSortKth(int[] nums, int k, int left, int right){
int mid =left + (right-left) / 2;
swap(nums,mid, right); // 将切分值放到右边界避免加入元素的划分
int partition = nums[right], i=left, j=right; // 双指针从左右边界开始,分别找到要交换的元素
while(i<j){
while(i<j && nums[i]>=partition) i++;
while(j>i && nums[j]<=partition) j--; // 找到右侧大于切分值的元素【因为是找大于,即使j从right开始,right也不会被选中】
swap(nums,i,j);
}
swap(nums,i,right); // i最后停留的位置一定是右侧首个小于切分值的元素,与切分值交换,则[left, i)都是大于(等于)切分值,[i+1, right]都是小于(等于)切分值
if(i==k-1) return nums[i];
if(i<k-1) return quickSortKth(nums,k,i+1,right);
return quickSortKth(nums,k,left,i-1);
}
private void swap(int[] nums, int i, int j){
if(i == j) return;
nums[i] ^= nums[j];
nums[j] ^= nums[i];
nums[i] ^= nums[j];
}
}
3.堆排序
堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆是一种特殊的完全二叉树,完全二叉树是每层都被完全填满的二叉树,除了最后一层可以不完全填满,并且最后一层的节点都集中在左侧。
- 第一个叶子节点,一定是序列长度/2,所以最后一个非叶子节点的下标就是arr.length / 2 -1。
- 节点i的左右节点是2i+1和2i+2
建大根堆的两种方法:
- heapInsert向上调整,当arr[i]>arr[(i-1)/2]时,就交换,i=(i-1)/2更新
- heapify向下调整,与左、右孩子中较大的比较、交换,注意:左右孩子存在与否,用size控制,需要判断。
堆排序的基本思想是:
- 先变成大根堆,heapInsert建堆,一个点一个点插入
- 大数归位:顶与尾交,size--,heapify循环至size>1.
public static int[] sortArray(int[] nums) {
if (nums.length > 1) {
// heapSort1为从顶到底建堆然后排序
// heapSort2为从底到顶建堆然后排序
// 用哪个都可以
// heapSort1(nums);
heapSort2(nums);
}
return nums;
}
// i位置的数,向上调整大根堆
// arr[i] = x,x是新来的!往上看,直到不比父亲大,或者来到0位置(顶)
public static void heapInsert(int[] arr, int i) {
while (arr[i] > arr[(i - 1) / 2]) {
swap(arr, i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
// i位置的数,变小了,又想维持大根堆结构
// 向下调整大根堆
// 当前堆的大小为size
public static void heapify(int[] arr, int i, int size) {
int l = i * 2 + 1;
while (l < size) {
// 有左孩子,l
// 右孩子,l+1
// 评选,最强的孩子,是哪个下标的孩子
int best = l + 1 < size && arr[l + 1] > arr[l] ? l + 1 : l;
// 上面已经评选了最强的孩子,接下来,当前的数和最强的孩子之前,最强下标是谁
best = arr[best] > arr[i] ? best : i;
if (best == i) {
break;
}
swap(arr, best, i);
i = best;
l = i * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
// 从顶到底建立大根堆,O(n * logn)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort1(int[] arr) {
int n = arr.length;
for (int i = 0; i < n; i++) {
heapInsert(arr, i);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
// 从底到顶建立大根堆,O(n)
// 依次弹出堆内最大值并排好序,O(n * logn)
// 整体时间复杂度O(n * logn)
public static void heapSort2(int[] arr) {
int n = arr.length;
for (int i = n - 1; i >= 0; i--) {
heapify(arr, i, n);
}
int size = n;
while (size > 1) {
swap(arr, 0, --size);
heapify(arr, 0, size);
}
}
347. 前 K 个高频元素
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> times = new HashMap<>();
for(int num : nums){
times.put(num, times.getOrDefault(num, 0)+1);
}
// 注意建小顶堆,更新堆中第k个值
PriorityQueue<int[]> queue = new PriorityQueue<>((a,b)->a[1]-b[1]);
for(Map.Entry<Integer,Integer> entry : times.entrySet()){
int num = entry.getKey(), count = entry.getValue();
if(queue.size()==k){
if(queue.peek()[1]<count){
queue.poll();
queue.offer(new int[]{num, count});
}
}else{
queue.offer(new int[]{num,count});
}
}
int[] ans = new int[k];
for(int i=0; i<k; i++){
ans[i] = queue.poll()[0];
}
return ans;
}
}
239.滑动窗口最大值 - 力扣(LeetCode)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
PriorityQueue<int[]> queue = new PriorityQueue<>((a,b)-> a[0] != b[0] ? b[0]-a[0] : b[1]-a[1]);
for(int i=0; i<k; i++){
queue.offer(new int[]{nums[i],i});
}
int[] ans = new int[n-k+1];
ans[0] = queue.peek()[0];
for(int i=k; i<n; i++){
queue.offer(new int[]{nums[i],i});
while(queue.peek()[1] <= i-k){
queue.poll();
}
ans[i-k+1] = queue.peek()[0];
}
return ans;
}
}
23.合并K个升序链表. - 力扣(LeetCode)
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> heap = new PriorityQueue<>((a,b) -> a.val-b.val);
// 遍历每一个头结点
for(ListNode h : lists){
if(h != null){
heap.add(h);
}
}
if(heap.isEmpty()){
return null;
}
// 记录返回的头结点
ListNode head = heap.poll();
// 记录弹出的结点
ListNode pre = head;
// 一定要注意,不要忘了把这个头结点的next加入小根堆
if(head.next != null){
heap.add(pre.next);
}
while(!heap.isEmpty()){
ListNode cur = heap.poll();
if(cur.next != null){
heap.add(cur.next);
}
pre.next = cur;
pre = cur;
}
return head;
}
}
线段重合_牛客题霸_牛客网 (nowcoder.com)
首先该题隐含的意思是:任何重合区域的左边界都一定是某个线段的左边界。因此,按照线段的开始位置进行从小到大排序。
接着用一个小根堆来存放结束位置。
遍历每个线段,若是 peek堆顶最小的元素 <= 当前线段的开始位置,就将堆顶元素弹出,然后将当前元素的结束位置加入小根堆。用当前堆的大小与ans PK去更新ans,得到重合最多的线段数。
import java.util.Scanner;
import java.util.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextInt()) { // 注意 while 处理多个 case
int n = in.nextInt();
int[][] line = new int[n][2];
for(int i=0; i<n; i++){
for(int j=0; j<2; j++){
line[i][j] = in.nextInt();
}
in.nextLine();
}
Arrays.sort(line,(a,b)->a[0]-b[0]);
PriorityQueue<Integer> queue = new PriorityQueue<>();
int ans =0;
for(int i=0; i<n; i++){
while(!queue.isEmpty() && queue.peek() <= line[i][0]){
queue.poll();
}
queue.offer(line[i][1]);
ans = Math.max(ans, queue.size());
}
System.out.println(ans);
}
in.close();
}
}
4.桶排序
桶排序的基本思想是将待排序的数据分配到几个有序的桶中,然后对每个桶内部进行排序。桶内数据排序后,按照顺序依次从桶中取出数据,组成的序列即有序。桶排序适合在外部排序中使用,例如处理大量存储在磁盘上的数据。
5.基数排序
样本是10进制的非负整数
基数排序则是一种按位进行排序的方法,通过多次使用桶排序或计数排序来分别对数据的每一位进行排序,最终得到完全有序的数据。基数排序要求数据可以分割成独立的“位”来进行比较,且每一位的数据范围不能太大。
6.计数排序
是一种特殊的桶排序,对数据范围小的有效。通过计数数组来记录每个值的频次,然后依次累加,最终得到每个元素在排序后的位置。计数排序的时间复杂度为O(n+k),空间复杂度也为O(n+k),其中k为数据的范围大小。
7.冒泡排序
从下标较小的元素开始,依次*对相邻两个元素的值进行两两比较*,若发现*逆序则交换*,使值较大的元素逐渐从前移向后部,就如果水底下的气泡一样逐渐向上冒。
定义一个标志位flag,用于判定元素之间是否进行了交换
在进行完一轮的排序之后,判断本轮是否发生了元素之间的交换,如果没有发生交换,说明数组已经是有序的了,则直接结束排序如果发生了交换,那么在下一轮排序之前将flag再次置为false,以便记录下一轮排序的时候是否会发生交换。
8.选择排序
从数组中选择最小元素,将它与数组的第一个元素交换位置。再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。
9.插入排序
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
插入排序的时间复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么逆序较少,需要的交换次数也就较少,时间复杂度较低。
10.希尔排序
希尔排序是一种基于插入排序的算法,通过将待排序数组按照一定的间隔分组,对每个分组进行插入排序,逐渐缩小间隔,最终使得整个数组有序。