排序算法汇总

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区域的左右边界
  1. <x,first与i交换,i++,first++
  2. ==x,i++
  3. >x,last与i交换,last--,i不变
  4. 当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个最大元素.

将大于切分值的元素都放到左侧,小于切分值得元素都放到右侧。因此我们可以使用双指针分别从两端往中间搜索,分别找到左侧小于切分值的元素和右侧大于切分值的元素,交换之,直到两个指针相遇。

对于这道题,我们可以选择区间的中间值作为切分元素,并且我们要事先将切分值交换到区间的右边界:

  1. 避免在划分左右区间的时候,将切分值给覆盖掉。
  2. 最终才可以将切分值放到正确的位置。
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

建大根堆的两种方法:

  1. heapInsert向上调整,当arr[i]>arr[(i-1)/2]时,就交换,i=(i-1)/2更新
  2. heapify向下调整,与左、右孩子中较大的比较、交换,注意:左右孩子存在与否,用size控制,需要判断。

堆排序的基本思想是:

  1. 先变成大根堆,heapInsert建堆,一个点一个点插入
  2. 大数归位:顶与尾交,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.希尔排序

希尔排序是一种基于插入排序的算法,通过将待排序数组按照一定的间隔分组,对每个分组进行插入排序,逐渐缩小间隔,最终使得整个数组有序。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值