【无标题】

昨天面试被问到了排序,说一种最熟悉的排序算法,说了快排,感觉答的不好,今天来复盘一下。

点击我的leetcode主页跳转:https://leetcode-cn.com/u/zhenglin_li/

问:排序算法分哪几种?

答:外排序和内排序
复盘:感觉他想问的是分为

1、基于插入的排序 :直接插入排序 、希尔排序

2、基于交换到排序 :冒泡排序 、快速排序

3、基于选择的排序 :简单选择排序、堆排序

问:知道哪几种?

在这里插入图片描述

冒泡排序

一句话总结:每次比较相邻的两个元素,逆序则交换,每一轮结束后最大的元素就会被换到结尾

  • 优化1,除了第一轮之后的每一轮的比较不用到尾,且每轮比较的次数越来越少

  • 优化2,记录每一趟是否交换过,如没有交换过,说明已经有序,提前返回

  • 复杂度:最好O(n),如果有序,没有交换过,一趟遍历即返回,平均O(n^2)

for (int i = 1; i < arr.length; i++) {  
	// 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。  
	boolean flag = true;  
	 
	for (int j = 0; j < arr.length - i; j++) {  
		if (arr[j] > arr[j + 1]) {  
			int tmp = arr[j];  
			arr[j] = arr[j + 1];  
			arr[j + 1] = tmp;  
	 
			flag = false;  
		}  
	}  
	 
	if (flag) {  
		break;  
	}  
}	

选择排序

一句话总结:每一轮是选择剩下的最小的,与第一个交换就行
复杂度最好最坏都是O(n^2),无论如何每一轮都要遍历来选择剩下的最小的

  • 算法思想 1:贪心算法:每一次决策只看当前,当前最优,则全局最优。注意:这种思想不是任何时候都适用。

  • 算法思想 2:减治思想:外层循环每一次都能排定一个元素,问题的规模逐渐减少,直到全部解决,即「大而化小,小而化了」。运用「减治思想」很典型的算法就是大名鼎鼎的「二分查找」。

  • 优点:交换次数最少。如果在交换成本较高的排序任务中,就可以使用「选择排序」(《算法 4》相关章节课后练习题)

// 总共要经过 N-1 轮比较  
for (int i = 0; i < arr.length - 1; i++) {  
    int min = i;  
  
 	// 每轮需要比较的次数 N-i  
 	for (int j = i + 1; j < arr.length; j++) {  
        if (arr[j] < arr[min]) {  
            // 记录目前能找到的最小值元素的下标  
 			min = j;  
 		}  
    }  
  
    // 将找到的最小值和i位置所在的值进行交换  
 	if (i != min) {  
        int tmp = arr[i];  
 		arr[i] = arr[min];  
 		arr[min] = tmp;  
 	}

插入排序

一句话总结:对于每一个元素a,他之前的元素依次后移一位,直到a在合适的位置上

  • 特点:「插入排序」在「几乎有序」的数组上表现良好。在数组「几乎有序」的前提下,「插入排序」可以提前终止内层循环;「短数组」的特点是:每个元素离它最终排定的位置都不会太远

  • 最好复杂度:O(n),如果有序,每一个元素只需要和前一位元素比较

for (int i = 1; i < arr.length; i++) {  
  
 	// 记录要插入的数据 a 
 	int tmp = arr[i];  
  
 	// 从已经排序的序列最右边的开始比较,找到比其小的数  
 	int j = i;  
 	while (j > 0 && tmp < arr[j - 1]) {  
 		arr[j] = arr[j - 1];  
 		j--;  
 	}  
  
 	// 存在比其小的数,插入  
 	if (j != i) {  
 		arr[j] = tmp;  
 	}  
 }

希尔排序

一句话总结:分治思想在简单插入排序上的应用,原序列分组为很多子序列,不断减少步长,直到为1

int length = arr.length;  
int temp;
// 步长为step,结束标志为step==1,每次缩短为原来的一半
for (int step = length / 2; step >= 1; step /= 2) {  
    for (int i = step; i < length; i++) {  
        temp = arr[i];  
 		int j = i - step;  
 		while (j >= 0 && arr[j] > temp) {  
            arr[j + step] = arr[j];  
 			j -= step;  
 		}  
        arr[j + step] = temp;  
		}  
}

归并排序

「归并排序」有「原地 + 迭代」和「借助额外空间 + 递归」,也即「自底向上」和「自顶向下」

自底向上「借助额外空间 + 递归」

自顶向下的归并排序进行的操作主要就是对数组的拆分与合并。通过层层拆分得到单元素数组,天生有序,然后归并两个单元素数组得到一个较大的有序数组,接着再归并两个较大数组得到更大的一个有序数组,重复这个过程,最终归并便得到了一个排好序的数组。

	public int[] sortArray(int[] nums) {
        return mergeSort(nums, 0, nums.length - 1);
    }

    public int[] mergeSort(int[] nums, int left, int right) {
        if (left >= right)
            return new int[]{nums[left]};
        int middle = (left + right) >>> 1;
        int[] leftPart = mergeSort(nums, left, middle);
        int[] rightPart = mergeSort(nums, middle + 1, right);
        return merge(leftPart, rightPart);
    }

    public int[] merge(int[] left, int[] right) {
        int[] res = new int[left.length + right.length];
        int i = 0, j = 0, index = 0;
        while (i < left.length && j < right.length)
            res[index++] = left[i] < right[j] ? left[i++] : right[j++];
        while (i < left.length)
            res[index++] = left[i++];
        while (j < right.length)
            res[index++] = right[j++];
        return res;
    }

时间复杂度分析:为什么为O(nlogn),且这个时间复杂度是稳定的,不随需要排序的序列不同而产生波动?
我们知道,归并排序的过程中,需要对当前区间进行对半划分,直到区间的长度为1。也就是说,每一层的子区间,长度都是上一层的1/2这也就意味着,当划分到第logn层的时候,子区间的长度就是1了。而归并排序的merge操作,则是从最底层开始(子区间为1的层),对相邻的两个子区间进行合并,对于每一层来说,在合并所有子区间的过程中,n个元素都会被操作一次,所以每一层的时间复杂度都是O(n)

每一层的时间复杂度为O(n),共有logn层,所以归并排序的时间复杂度就是O(nlogn)

自顶向下「原地 + 迭代」

在这里插入图片描述

归并两个单元素数组得到一个较大的有序数组,接着再归并两个较大数组得到更大的一个有序数组,重复这个过程,最终归并便得到了一个排好序的数组。

快速排序

一句话总结:快速排序每一次都排定一个元素(这个元素呆在了它最终应该呆的位置),然后递归地去排它左边的部分和右边的部分,依次进行下去,直到数组有序
算法思想:分而治之(分治思想),与「归并排序」不同,「快速排序」在「分」这件事情上不想「归并排序」无脑地一分为二,而是采用了 partition 的方法(每次partition后这个元素呆在了它最终应该呆的位置),因此就没有「合」的过程。

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

    public void quickSort(int[] nums, int start, int end) {
        if (start >= end)
            return;
        int partitionIndex = partition(nums, start, end);
        // 注意要-1
        quickSort(nums, start, partitionIndex - 1);
        quickSort(nums, partitionIndex + 1, end);
    }

    public int partition(int[] nums, int left, int right) {
        int pivot = left;
        while (left < right) {
            // 注意先right--再left++
            while (left < right && nums[right] > nums[pivot])
                right--;
            // 注意要有等号,二选一
            while (left < right && nums[left] <= nums[pivot])
                left++;
            if (left < right)
                swap(nums, left, right);
        }
        // now left == right is true
        swap(nums, pivot, left);
        return left;
    }

    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

时间复杂度分析:

  • 最差情况:选取的轴刚好就是这个区间的最大值或者最小值。需要处理n轮,每轮复杂度为O(n)

  • 最好情况,我们选取的轴刚好就是这个区间的中位数。
    也就是说,在操作之后,正好将区间分成了数字个数相等的左右两个子区间。此时就和归并排序基本一致了。

堆排序

一句话总结:先建堆,再从后往前对每一个元素 交换和归位

堆:完全二叉树 + parent > children

注意在堆排序中,建堆堆复杂度为O(n),前提是在堆排序中,否则更普适的情况是O(nlogn)

class Solution {
    public int[] sortArray(int[] nums) {
        heapSort(nums);
        return nums;
    }

    // 堆排序,先建堆,在进行交换和归位
    public void heapSort(int[] nums) {
        buildHeap(nums);
        for (int i = nums.length - 1; i >= 1; i--) {
            swap(nums, 0, i);
            siftDown(nums, 0, i - 1);
        }
    }

    // 归位函数,用于递归地把某个节点放到该放的位置
    public void siftDown(int[] nums, int root_index, int end) {
        int max_index = root_index;
        int left_index = root_index * 2 + 1;
        int right_index = root_index * 2 + 2;
        if (left_index <= end && nums[left_index] > nums[max_index])
            max_index = left_index;
        if (right_index <= end && nums[right_index] > nums[max_index])
            max_index = right_index;
        if (max_index != root_index) {
            swap(nums, max_index, root_index);
            siftDown(nums, max_index, end);
        }
    }

    // 建堆,O(n),从倒数第二层有叶子节点的那个节点开始,依次递减
    public void buildHeap(int[] nums) {
        int index_tail = nums.length - 1;
        int index_parent = (index_tail - 1) / 2;
        for (int i = index_parent; i >= 0; i--)
            siftDown(nums, i, index_tail);
    }

    public static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值