快速排序
快速排序体现了分治的思想。每次决定一个数的最终位置,将其放置到正确位置后,再通过递归的方式将两头的数组按照同样的方式快速排序。
需要注意的是,使用快速排序时,需要随机取那个决定数,这叫做随机选择比较子。因为当数组时已排好序的时候,指针只会从尾刷到头,快速排序的效率就会非常低,时间复杂度为O(n^2)。所以随机选择比较子后,将比较子和数组最左边的数交换,再做排序。这样的话就算数字已经排好序了,指针也不会从尾刷到头。
- 快速排序是一种不稳定的算法。算法不稳定是指:在排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序。
假设待排序数组: a = [ 1, 2, 2, 3, 4, 5, 6 ];
在快速排序的随机选择比较子(即pivot)阶段:
若选择a[2](即数组中的第二个2)为比较子,而把大于等于比较子的数均放置在大数数组中,则a[1](即数组中的第一个2)会到pivot的右边, 那么数组中的两个2非原序(这就是“不稳定”)。
若选择 a[1] 为比较子,而把小于等于比较子的数均放置在小数数组中,则数组中的两个 2 顺序也非原序 。
这就说明,quick sort是不稳定的。
- 时间复杂度:O(nlogn),这里n是数组的长度;
- 空间复杂度:O(logn),这里占用的空间主要来自递归函数的栈空间。
class Solution{
Random random = new Random();
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length-1);
return nums;
}
//快速排序
public void quickSort(int[] nums, int start, int last){
if(start<last){
int left = start, right = last;
//随机取数
int ranIndex = random.nextInt(last-start+1)+start;
int temp = nums[ranIndex];
nums[ranIndex] = nums[left];
nums[left] = temp;
int aim = nums[left];
while(left<right){
while(left<right && aim<=nums[right]) right--;
if(left<right) nums[left++] = nums[right];
while(left<right && nums[left]<=aim) left++;
if(left<right) nums[right--] = nums[left];
}
nums[left] = aim;
quickSort(nums, start, left-1);
quickSort(nums, left+1, last);
}
}
}
也可以使用交换的方法,每次找到左右指针与顺序不一样的第一个下标,然后左右指针的数进行交换,这种方法可以少很多次赋值,但是这也要记得temp的位置(left)要和指针开始的移动位置相反(right指针先动)。
public void quickSort(int[] nums, int start, int last){
if(start<last){
int p = partition(nums, start, last);
quickSort(nums, start, p-1);
quickSort(nums, p+1, last);
}
}
public int partition(int[] nums, int left, int right){
int randomNum = random.nextInt(right-left+1) + left;
swap(nums, randomNum, left);
int temp = nums[left];
int i = left, j = right;
while(i<j){
while(i<j && nums[j]>=temp) j--;
while(i<j && nums[i]<=temp) i++;
if(i<j) swap(nums, i, j);
}
swap(nums, left, i);
return i;
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
归并排序
归并排序也是体现了分治的思想。将排序任务分给两个子数组,再将两个子数组合并。这里合并的时候需要多申请n的空间资源存放排序后的数组。
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
class Solution {
public int[] sortArray(int[] nums) {
int[] buffer = new int[nums.length];
mergeSort(nums, 0, nums.length-1, buffer);
return nums;
}
public void mergeSort(int[] nums, int start, int last, int[] buffer){
if(start<last){
int mid = (start+last)/2;
mergeSort(nums, start, mid, buffer);
mergeSort(nums, mid+1, last, buffer);
mergeTwoArray(nums, start, mid, last, buffer);
}
}
public void mergeTwoArray(int[] nums, int start, int mid, int last, int[] buffer){
int left = start, right = mid+1;
int i = left;
while(left<=mid && right<=last){
if(nums[left]<=nums[right]){
buffer[i++] = nums[left++];
} else{
buffer[i++] = nums[right++];
}
}
while(left<=mid) buffer[i++] = nums[left++];
while(right<=last) buffer[i++] = nums[right++];
for(int j = start; j<=last; j++){
nums[j] = buffer[j];
}
}
}
选择排序
选择排序适合于交换数据代价比较大的场合。每轮记住未排序数组中最小的下标,遍历完数组后把最小的下标数和排序后的下一个数(未排序数)交换。
- 时间复杂度:O(n^2)
- 空间复杂度:O(1)
public class Solution {
// 选择排序:每一轮选择最小元素交换到未排定部分的开头
public int[] sortArray(int[] nums) {
int len = nums.length;
// 循环不变量:[0, i) 有序,且该区间里所有元素就是最终排定的样子
for (int i = 0; i < len - 1; i++) {
// 选择区间 [i, len - 1] 里最小的元素的索引,交换到下标 i
int minIndex = i;
for (int j = i + 1; j < len; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums, i, minIndex);
}
return nums;
}
private void swap(int[] nums, int index1, int index2) {
int temp = nums[index1];
nums[index1] = nums[index2];
nums[index2] = temp;
}
插入排序
插入排序在数组越有序的时候效率越高,越有序,需要移动指针的次数越少,最少只需要n次。
插入排序每次从左侧已排序数组的下一个数(未排序数)开始与已排序数组的数逐一比较,若未排序数更小,那就把大的已排序数往后挪一格,直到找到属于自己的位置。
class Solution{
public void InsertSort(int[] nums){
for(int i = 0; i<nums.length; i++){
int temp = nums[i];
int j = i;
while(j>0 && nums[j-1]>temp){
nums[j] = nums[j-1];
j--;
}
nums[j] = temp;
}
}
}
堆排序
堆排序更适合去算那些前k个或者第k个数的题目。堆排序是根据大顶堆或者小顶堆的规则实现的。
- 大顶堆:每个节点的值都大于或者等于它的左右子节点的值,计算升序用大顶堆。
- 小顶堆:每个节点的值都小于或者等于它的左右子节点的值,计算降序用小顶堆。
堆排序本身的排序思想是:
- 创建一个大顶堆或者小顶堆。从第一个非叶子节点开始从下往上按大顶堆/小顶堆的规则交换节点。
- 每次堆顶和堆尾交换,将交换到堆尾的元素排除堆中,再去处理交换到堆头的元素保证符合大顶堆/小顶堆的规则。
根据大顶堆的性质,每个节点的值都大于或者等于它的左右子节点的值。所以我们需要找到所有包含子节点的节点,也就是非叶子节点,然后调整他们的父子关系,非叶子节点遍历的顺序应该是从下往上,这比从上往下的顺序遍历次数少很多,因为,大顶堆的性质要求父节点的值要大于或者等于子节点的值,如果从上往下遍历,当某个节点即是父节点又是子节点并且它的子节点仍然有子节点的时候,因为子节点还没有遍历到,所以子节点不符合大顶堆性质,当子节点调整后,必然会影响其父节点需要二次调整。但是从下往上的方式不需要考虑父节点,因为当前节点调整完之后,当前节点必然比它的所有子节点都大,所以,只会影响到子节点二次调整。相比之下,从下往上的遍历方式比从上往下的方式少了父节点的二次调整。
那么,该如何知道最后一个非叶子节点的位置,也就是索引值?
对于一个完全二叉树,在填满的情况下(非叶子节点都有两个子节点),每一层的元素个数是上一层的二倍,根节点数量是1,所以最后一层的节点数量,一定是之前所有层节点总数+1,所以,我们能找到最后一层的第一个节点的索引,即节点总数/2(根节点索引为0),这也就是第一个叶子节点,所以第一个非叶子节点的索引就是第一个叶子结点的索引-1。那么对于填不满的二叉树呢?这个计算方式仍然适用,当我们从上往下,从左往右填充二叉树的过程中,第一个叶子节点,一定是序列长度/2,所以第一个非叶子节点的索引就是arr.length / 2 -1。
public class Main {
private static final Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) {
Main main = new Main();
int[] nums = new int[]{3,2,1,5,6,4,8,2,4};
main.heapSort(nums);
for(int i: nums){
System.out.println(i);
}
}
public void heapSort(int[] nums) {
int len = nums.length;
if(nums == null || len==0){
return;
}
//创建一个大顶堆
buildMaxHeap(nums, len);
//每次取大顶堆的顶与数列尾部交换,之后不纳入计算堆顶的范围内
for(int i = len-1; i>0; i--){
swap(nums, 0, i);
//使得i与其左右子节点保持大顶堆规则
heapify(nums, 0, i-1);
}
}
public void buildMaxHeap(int[] nums, int len) {
//第一个非叶子节点开始往上遍历
for(int i = len/2-1; i>=0; i--){
heapify(nums, i, len);
}
}
public void heapify(int[] nums, int i, int len) {
int left = 2*i+1, right = 2*i+2;
int largestIndex = i;
if(left<len && nums[largestIndex]<nums[left]){
largestIndex = left;
}
if(right<len && nums[largestIndex]<nums[right]){
largestIndex = right;
}
if(largestIndex!=i){
swap(nums, i, largestIndex);
heapify(nums, largestIndex, len);
}
}
public void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
堆排序可以用于计算前k个最大的数/频率最高的k个数。
Java有以堆为实现的数据结构:优先队列。优先队列的底层实现是最小堆,将数据offer()加入到优先队列后,poll()出来的第一个数就是最小堆的堆顶,也就是堆中最小的数,当数据放入的时候,自动会按照最小堆的逻辑更新数据顺序,以此可以直接算出很多结果。
例如:计算前K个高频数字。
这种题目就可以通过先用HashMap计算每个数字的出现频率,再用优先队列存放数字和频率的键值对,自定义Comparator。我们的目的是得到一个存放前K个高频数字的堆。
做法:不断将键值对存入优先队列,当优先队列的数量达到K时不再添加,而是每次取堆顶元素的频率值和当前频率值比。如果堆顶更小则去掉当前堆顶,加入当前频率值;如果当前值更小,那么下一个循环。当循环结束后,优先队列中存的就是前K个高频数字和它的频率了。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
for(int i: nums){
if(!map.containsKey(i)){
map.put(i, 0);
}
map.put(i, map.get(i)+1);
}
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
public int compare(int[] arr1, int[] arr2){
return arr1[1] - arr2[1];
}
});
int j = 0;
for(Map.Entry<Integer, Integer> entry: map.entrySet()){
int num = entry.getKey(), freq = entry.getValue();
if(j<k){
queue.offer(new int[]{num, freq});
} else{
int minFreq = queue.peek()[1];
if(freq>minFreq){
queue.poll();
queue.offer(new int[]{num, freq});
}
}
j++;
}
int[] ret = new int[k];
for(int i = 0; i<k; i++){
ret[i] = queue.poll()[0];
}
return ret;
}
}
参考:
https://blog.csdn.net/qq_28063811/article/details/93034625/