【剑指offer】【java】【排序】归并排序 快排 堆排的总结及java实现

选择排序

从后面选择最小的值添加到前面已经排好序的下一位。
第一轮下来,放在第一位的是整个数组中的最小值


public void sort(T[] nums){
	int N = nums.length;
	//第一层for循环用于 遍历已经排好序的前面部分
	for(int i=0;i<N-1;i++){
		int min = i;
		for(int j=i+1;j<N;j++){
			if(nums[j]<nums[min])
				min = j;
		}
		swap(nums,i,min);
	}

}

冒泡排序

左到右不断交换相邻的逆序元素。
第一轮,会让最大的元素排到最后一位。
可以添加一个flag,判断,如果某一轮中没有发生交换,那么此时这个数组是有序的,可以直接退出for循环。

public void sort(T[] nums){
	int N = nums.length;
	boolean isSort = false;
	//因为排好序的都在后面,所以第一层 先从后面开始for循环
	for(int i=N-1;i>0&&!isSort;i--){
		isSort=true;
		//如果下面for循环有冒泡交换,则置为false,如果没有冒泡交换,则为true,可直接break for循环
		for(int j=0;j<i;j++){
			if(nums[j+1]<nums[j]){
				isSort=false;
				swap(nums,j,j+1);
			}
		}
	}

}

插入排序

每次将当前元素插入到左侧已排序的数组中,使得插入之后的左侧数组依然有序。

public void sort(T[] nums){
	int N = nums.length;
	//外层for循环 遍历前面部分已排序
	for(int i=1;i<N;i++){
	//用已排序的后一位,与前面所有的已排序的比较
		for(int j=i;j>0;j--){
			if(nums[j]<nums[j-1]){
				swap(nums,j,j-1);
			}
		}
	}
}

归并排序

将数组中的两个已经排序的部分归并成一个;
主要分成:

  • 自顶向下的归并排序:将一个大数组分成两个小数组去求解,每次将问题对半分成两个子问题。
  • 自底向上的归并排序:先归并那些微型数组,然后成对归并得到的微型数组。


1.归并排序的思想

主要思想分成三步:
1.将序列中的待排序的数字分成若干组,每个数字分成一组。
2.将若干组数字中两两进行合并,保证合并后的组是有序的。
3.重复第二步操作 直到剩下最后一组,排序完成

  • 1.找到mid
  • 2.递归mid左边的部分
  • 3.递归mid右边的部分
  • 4.把两个结果再merge
      1. 先比较两个小数组相应的下标位置所对应的数组值的大小。小的先放进新数组
      1. 如果左边或者右边有剩余,则剩余部分拷贝至tmparray
      1. 把最后结果返回给nums数组
        参考:https://blog.csdn.net/hd12370/article/details/81036680
public class mergeSort{
	
	public static void merge(int[] nums,int left,int mid,int right){
		if(left<right){
			int[] tmpArr = new int[nums.length];
			int rightStart = mid+1;
			int tmp=left,i=left;
			while(left<=mid&&right<=right){
				if(nums[left]<=nums[rightStart]){
					tmpArr[i]=nums[left];
					i++;
					left++;
				}else{
					tmpArr[i]=nums[rightStart];
					i++;
					rightStart++;
				}
			}
			while(left<=mid){
				tmpArr[i]=nums[left];
				i++;
				left++;
			}
			while(rightStart<=right){
				tmpArr[i]=nums[rightStart];
				i++;
				rightStart++;
			}
			while(tmp<=right){
				nums[tmp]=tmpArr[tmp];
				tmp++;
			}
		}
	}
	
	public static void sort(int[] nums,int left,int right){
		if(left<right){
			int mid = 1+(right-left)/2;
			sort(nums,left,mid);
			sort(nums,mid+1,right);
			merge(nums,left,mid,right);
		}
	}
	 

}
2.用归并思想给链表排序如何写

题目:在O(n log n)的时间内使用常数级空间复杂度对链表进行排序。
Sort a linked list in O(n log n) time using constant space complexity.

思路:用归并排序给链表排序
按照上面的思路步骤:

  • 1.找到mid(快慢指针)
  • 2.递归mid左边的部分(head到mid 注意这里mid.next==null截断前部分链表)
  • 3.递归mid右边的部分
  • 4.把两个结果再merge
      1. 对比两个链表,小的放进tmp中
      1. 如果左边或者右边有剩余,则剩余部分拷贝至tmp
      1. 把最后结果返回
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    
    public static ListNode mergeSort(ListNode l1,ListNode l2){
        ListNode pre = new ListNode(-1);//创建一个新的node
        ListNode tmp1=l1;
        ListNode tmp2=l2;
        ListNode tmp=pre;
        while(tmp1!=null&&tmp2!=null){
            if(tmp1.val<tmp2.val){
                tmp.next=tmp1;
                tmp1=tmp1.next;
            }else{
                tmp.next = tmp2;
                tmp2=tmp2.next;
            }
            tmp = tmp.next;
        }
        if(tmp1==null){
            tmp.next = tmp2;
        }
        if(tmp2==null){
            tmp.next = tmp1;
        }
        return pre.next;
    }
    
    //用快慢指针获取中间mid
    public static ListNode getMid(ListNode head){
        if(head==null||head.next==null){
            return head;
        }
        ListNode slow = head, quick = head;
        while(quick.next != null && quick.next.next != null) {
            slow = slow.next;
            quick = quick.next.next;
        }
        return slow;
        
    } 
    
    public ListNode sortList(ListNode head) {
        if(head==null||head.next==null){
            return head;
        }
        ListNode mid = getMid(head);
        ListNode midNext = mid.next;
        mid.next = null;//分成两个链表,前半部分的链表尾节点的next设为null
        return mergeSort(sortList(head), sortList(midNext));
    }
    
    
}
3.时间复杂度分析
4.用归并思想求数组中逆序对数如何写?

这个题目是剑指offer中的数组中的逆序对,使用归并解决,

public class Solution {
    
    private long cnt =0;
    private int[] tmp;
    public int InversePairs(int [] array) {
        tmp = new int[array.length];
        mergeSort(array,0,array.length-1);
        return (int)(cnt%1000000007);
    }
    public void mergeSort(int[] nums,int l,int h){
        if(h-l<1){
            return;
        }
        int mid = l+(h-l)/2;
        mergeSort(nums,l,mid);
        mergeSort(nums,mid+1,h);
        merge(nums,l,mid,h);
    }
    public void merge(int[] nums,int l,int m,int h){
        int i = l,j=m+1,k=l;
        while(i<=m||j<=h){
            if(i>m){
                tmp[k]=nums[j++];
            }else if(j>h){
                tmp[k]=nums[i++];
            }
            else if(nums[i]<nums[j]){
                tmp[k]=nums[i];
                i++;
            }else{
                tmp[k]=nums[j];
                j++;
                this.cnt+=(m-i+1);//这边cnt添加的是因为nums[i,...,mid]都大于nums[j]
            }
            k++;
        }
        for(k=l;k<=h;k++){
            nums[k]=tmp[k];
        }
    }
}

5.M个长度为N的数组如何排序?时间复杂度多少?
6.M个长度为N的链表如何排序?

快速排序

面试一般都希望能够精准无误的写出快排。只要记清楚算法的流程,带有模板式的记忆

1.快速排序的思想
挖坑填数思想进行排序:用左边第一个元素作为参照值,并挖出来,空出位置;然后从右边往左找到第一个小于参照值的数填进来;此时右边空出一个位置;然后从左边找到一个大于参照值的数,填到右边的空位,此时左边空出一个位置......不断左右找数填坑,最终左右遍历坐标相等时的坑位就是参照值的位置。由 挖坑填数 排序得到的参照值的位置划分序列,分别进行左右递归;

    第一步排序左右:选一个参照值,定义两个下标left和right,一个从头到尾,一个从尾到头遍历。各自找到一个大于/小于 参照值 的元素则停下,然后把这个元素与参照值进行交换,使得小于参照值的都在右边,大于的在右边。两下标相等时停止遍历。此时参照值的位置就划分了左右序列,把此时参照值位置返回。

    第二步递归左右:对第一步得到的参照值的左、右序列继续执行排序操作——选参照值、前后遍历与参照值比较交换,最终使参照值处于合适位置。

简洁的步骤:
1.递归
2.找中轴(所有左边的数都小于右边的数)
3.左边递归,右边递归
4.如何找中轴?
快排的递归实现步骤如下:(注意,所有循环的left皆要加left小于right)
在这里插入图片描述
java代码实现:

public class QuickSort {
    public int[] quickSort(int[] A, int n) {
        quick(A,0,n-1);
        return A;
    }
//由 挖坑填数 排序得到的参照值的位置划分序列,分别进行左右递归
    public void quick(int[] A,int left,int right){
        if(left<right){
            int mid=sort(A,left,right);//先进行左右排序,并把排序结果的分界点下标返回,作为递归的中间值
            quick(A,left,mid);//然后对左右排序的左序列、右序列进行递归
            quick(A,mid+1,right);
        }
    }
//由挖坑填数思想进行排序:用左边第一个元素作为参照值,并挖出来,空出位置;然后从右边往左找到第一个小于参照值的数填进来;此时右边空出一个位置;然后从左边找到一个大于参照值的数,
//填到右边的空位,此时左边空出一个位置......不断左右找数填坑,最终左右遍历坐标相等时的坑位就是参照值的位置。
    public int sort(int[] A,int left,int right){
        int i=left;
        int j=right;
        int temp=A[i];
        while(i<j){
            while(j>i && A[j]>=temp){//从右往左找到第一个小于参照值的元素
                --j;
            }
            if(j>i){//根据元素位置判断是否可以作出交换,是i则把参照值交换到右边,使找到的元素处于参照值左边,然后移动左边下标
                A[i++]=A[j];
            }
            while(i<j && A[i]<=temp){
                ++i;
            }
            if(i<j){
                A[j--]=A[i];
            }
        }
        if(i==j){
            A[i]=temp;//当左右遍历下标相等时,这个位置就是参照值的位置:左边都是小于它的数,右边都是大于它的数
        }
        return i;
    }
}
//复杂度计算:T=N(每次排序交换左右遍历N个元素)*logN(每次平分序列,logN次可以把序列分至单个元素一个区间)=O(N*logN)

复杂度计算:T=N(每次排序交换左右遍历N个元素)logN(每次平分序列,logN次可以把序列分至单个元素一个区间)=O(NlogN)

2.求无序数组中第k小

在这里插入图片描述

import java.util.*;
public class Main {
    
    public static int getIndex(int[] arr,int left,int right){
        int pivot = arr[left];
        while(left<right){
            while(left<right&&arr[right]>=pivot){
                right--;
            }
            if(left<right){
                arr[left] = arr[right];
                left++;
            }
            while(left<right&&arr[left]<=pivot){
                left++;
            }
            if(left<right){
                arr[right] = arr[left];
                right--;
            }
        }
        if(left==right){
            arr[left] = pivot;
        }
        return left;
    }
    
    public static void quickSort(int[] arr,int start,int end){
        if(start<end){
            int index = getIndex(arr,start,end);
            quickSort(arr,start,index);
            quickSort(arr,index+1,end);
        } 
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String[] line = sc.nextLine().replace("[","").replace("]","").split(",");
        int[] arr = new int[line.length];
        for(int i = 0;i<line.length;i++){
            arr[i] = Integer.parseInt(line[i]);
        }
        //查找排序后第三大的元素
        /*
        Arrays.sort(arr);
        */
        quickSort(arr,0,arr.length-1);
        System.out.println(arr[arr.length -3]);
        
        
        
    }
}
3.时间复杂度分析

复杂度计算:T=N(每次排序交换左右遍历N个元素)logN(每次平分序列,logN次可以把序列分至单个元素一个区间)=O(NlogN)

堆排序

堆是一颗完全二叉树
在这里插入图片描述

1.堆排序的思想

1.构造一个大顶堆,取堆顶数字(也就是最大值)
2.再将剩下的数字构建一个大顶堆,取堆顶数字(也就是盛夏之当中的最大值)
3.重复以上操作,直到取完堆中的数字,最终得到一个从大到小排列的序列

对于给定的某个结点的下标 i,可以很容易的计算出这个结点的父结点、孩子结点的下标:

Parent(i) = i/2,i 的父节点下标

Left(i) = 2i,i 的左子节点下标

Right(i) = 2i + 1,i 的右子节点下标

* 第一步:建一个最大堆:无序数组建立堆最直接的办法是从左到右遍历数组进行上浮或者从右至左进行下沉操作(更高效)。(包括局部构建最大堆;下沉)
* 第二步:交换堆顶元素与最后一个元素,交换之后需要进行下沉操作维持堆的有序状态()//将建好的堆进行排序(交换+下沉)

下图的操作是:for循环的下沉操作。
注意的点:
1.如果一个节点的两个子节点都已经堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。
2.因为叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。即 for循环 从length/2开始。

在这里插入图片描述

//手写一个堆排
public void sort(T[] nums){
	int N=nums.length-1;
	for(int k=N/2;k>=1;k--){
		sink(nums,k,N);
	}
	while(N>1){
		swap(nums,1,N--);
		sink(nums,1,N);
	}

}
//sink函数 nums数组 N 数组长度  k 需要下沉的位置
public void sink(int[] nums,int k,int N){
	while(2*k<=N){
		int j=2*k;
		if(j<N&&nums[j]<nums[j+1]){
			j++;
		}
		if(nums[k]>nums[j]){
			break;
		}
		swap(nums[k],nums[j]);
		k=j;
	}
}

2.符合堆的结构,插入和删除函数如何实现

1.上浮
在这里插入图片描述
2.下沉
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3.时间复杂度分析

1.堆的高度是logN,因此插入和删除最大元素的复杂度是logN;
2.对于堆排,由于需要对N个结点进行下沉操作,因此复杂度是NlogN

4.无序数组的topK问题

https://www.nowcoder.com/questionTerminal/673454422d1b4a168aed31e449d87c00?toCommentId=5293012
在这里插入图片描述
这个只能A 80%

//for heap
import java.util.*;
public class Main {
    public static void headSort(int[] nums){
        int N = nums.length-1;
        for(int k=N/2;k>=1;k--){
            sink(nums,k,N);
        }
        while(N>1){
            swap(nums,1,N);
            N--;
            sink(nums,1,N);
        }
        
    }
    
    public static void sink(int[] nums,int k,int N){
        while(2*k<=N){
            int j = 2*k;
            if(j<N&&nums[j]<nums[j+1]){
                j++;
            }
            if(nums[k]>=nums[j]){
                break;
            }
            swap(nums,k,j);
            k=j;
        }
    }
    
    public static void swap(int[] nums,int i,int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
    
    // main
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String[] line = sc.nextLine().replace("[","").replace("]","").split(",");
        int[] arr = new int[line.length];
        for(int i = 0;i<line.length;i++){
            arr[i] = Integer.parseInt(line[i]);
        }
        //查找排序后第三大的元素
        /*
        Arrays.sort(arr);
        */
        //quickSort(arr,0,arr.length-1);
        headSort(arr);
        System.out.println(arr[arr.length -3]);
       
        }
    }

上述代码只能A 80% 问题暂时没找出来
在这里插入图片描述
其他堆排解决:PriorityQueue

链接:https://www.nowcoder.com/questionTerminal/673454422d1b4a168aed31e449d87c00?toCommentId=5293012
来源:牛客网

 public static int findKthNum1(int data[], int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        for (int item : data) {
            if (heap.size() < k) {
                heap.add(item);
            } else if (item > heap.peek()) {
                heap.poll();
                heap.add(item);
            }
        }
        return heap.poll();
    }

总结

对于面试来说:快速排序 归并排序 堆排序 是一定要掌握的。
掌握标准:
1.一定非常熟练,时间复杂度也一定要特别熟悉。
2.快排如何写,什么思想?用快排思想求无序数组中第K小的值如何写?时间复杂度多少?
3.归并如何写?什么思想?用归并思想给链表排序如何写?用归并思想求数组中逆序对数如何写?M个长度为N的数组如何排序?时间复杂度多少?M个长度为N的链表如何排序?
4.堆排序如何写,什么思想?时间复杂度怎么算的?符合堆的结构,插入和删除函数如何实现?
把这几个问题弄会,三大排序问题基本就没问题了。

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值