目录
🌾前言
🌾 内部排序
🌈1. 直接插入排序
🌈2. 希尔排序
🌈3. 直接选择排序
🌈4. 堆排序
🌈5. 归并排序
🌈6. 冒泡排序
🌈7. 快速排序
🌾外部排序
🌾前言
今天学习的内容主要是七大排序方法,属于面试中经常被问到的问题😃。首先我们需要知道排序方法主要分为几类,可以看下面这张图。我们说的七大排序也是指内部排序的方式。其中,关于算法的稳定与不稳定,主要是指在排序过程中的元素位置是否发生变化。
举个栗子🌰:
[9,2,5a,7,5b,4,3,6]在经过直接选择排序后变为[2,3,4,5b,5a,6,7,9]其中5a与5b虽然是相同的值,但是相较于开始位置,两者之间的顺序已经发生变化,所以我们说直接选择排序算法是不稳定的。(其中5a,5b表示的意思是 两者都是相同的值5 ,这里用来区分一些排序前后的位置记录)
🌾 内部排序
🌈1. 直接插入排序
核心思路:将0-N索引的元素看作是有序的,将N+1索引后的元素到最后一个元素当做是无序的。遍历无序的数组,将遍历到的元素插入到有序序列的合适的位置,如果遇见相同的数据,插在后面即可。
插入排序再近乎有序的数组上排序效果非常好,经常作为其他高阶排序的优化手段。
代码实现:
public static void insetSort(int[] arr){
int startIndex = 0;
//先找到有序区间
for (int i = 0; i < arr.length-1; i++) {
if(arr[i] > arr[i+1]){
startIndex = i;
break;
}
}
//有序区间[0,i]
//无序区间[i+1,n)
for (int i = startIndex+1; i < arr.length; i++) {
//i当前无序区间的第一个值,j表示有序区间新增的值,然后每次新增一个值就要往前看不断调整有序区间使之有序。
for (int j = i; j >= startIndex; j--) {
if(arr[j] < arr[j-1]){
swap(arr,j,j-1);
}
}
}
}
//交换函数
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
🌈2. 希尔排序
希尔排序其实是对于插入排序的一种优化。我们知道,插入排序在近乎有序的数组上性能是很好的。所以利用这一特点,数组元素越少,这个数组越接近于有序状态。将原数组分成若干个子数组,先将子数组调整的内部有序,不断变大这个分组的长度,当最终分组长度为1时,整个数组接近于有序。最后进行一次插入排序即可。 gap一般为2或者是3。
//先不断按照gap分组,让gap不断/=2或者/=3,最后当gap=1时使用直接插入排序即可。
public static void heirSort(int[] arr){
// gap/2
int gap = arr.length >> 1;
while (gap > 1){
insetSortByGap(arr,gap);
gap = gap >> 1;
}
//此时gap为1,直接使用插入排序
insetSortByGap(arr,1);
}
//对每次分组的子数组进行排序
private static void insetSortByGap(int[] arr, int gap) {
//j原来是向前看一个单位,现在是向前看gap个单位
for (int i = gap; i < arr.length; i++) {
for (int j = i; j-gap > 0 ; j=j-gap) {
if(arr[j] < arr[j-gap]){
swap(arr,j,j-gap);
}
}
}
}
🌈3. 直接选择排序
核心思路:每次在无序区间中找最小值与无序区间的第一个值进行交换,直到数组有序。
public static void selectionSort(int[] arr){
//起始状态:有序区间[0,i)
//无序区间:[i,n)
int min = 0;
//min索引指向的值一定是无序区间的最小值
//i索引指向的值是无序区间的第一个值
for (int i = 0; i < arr.length-1; i++) {
min = i;
for (int j = i; j < arr.length; j++) {
if(arr[min] > arr[j]){
min = j;
}
}
//此时min指向的值一定是当前无序区间的最小值,换到无序区间的最开始位置
swap(arr,min,i);
}
}
后来又发现,最上面那个代码好的min和i其实刚开始就是一个值,min找到组小智知乎,又去和i做了一次交换,其实可以简化,这种简单的选择排序方式也就是这样:
public static void main(String[] args) {
/**
* 选择排序
* 1、从0索引开始,根后面的元素一一比较
* 2、小的元素放前面,大的放在后面
* 3、第一次循环结束,已经将最小值放在最左边
* 4、第二次循环从索引为1 的地方开始继续比较
*/
int[] arr = {2,5,1,7,4};
//外层表示比较的趟数
for (int i = 0; i < arr.length-1; i++) {
//内层比较
for (int j = i; j < arr.length; j++) {
if(arr[i] > arr[j]){
swap(arr,i,j);
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
🌈4. 堆排序
升序:建大堆;降序:建小堆
主要思路:先将数组调整为最大堆,不断的将当前堆顶元素和无序数组的最后一个元素进行交换,此时,无序数组的最大值就放在了最终位置,然后进行下沉操作。重复上述步骤,当无序数组的只剩下一个元素时,整个堆排序完成。
//堆排序
public static void heapSort(int[] arr){
//1、先将任意数组堆化,使其变为一个最大堆
//从最后一个非叶子节点不断向前看,进行下沉操作
for (int i = (arr.length-1-1) / 2; i > 0 ; i--) {
siftDown(arr,i,arr.length);
}
//2、不断地将当前无序数组的最大值(堆顶)和最后一个元素交换
for (int i = arr.length - 1; i > 0 ; i--) {
//将堆顶元素和i交换,i是无序数组的最后一个元素
swap(arr,0,i);
//交换完之后进行元素的下沉操作
siftDown(arr,0,i);
}
}
//在数组arr上进行元素下沉操作
private static void siftDown(int[] arr, int k, int size) {
//当前节点的左孩子节点
int j = 2 * k + 1;
while (j < size){
//在左子树存在的条件下
//如果右子树也存在,且右子树的值大于左子树
if(j + 1 < size && arr[j+1] > arr[j]){
//将j更新为最大值所在的索引
j = j + 1;
}
//此时j保存了左右子树最大值索引
if(arr[k] < arr[j]){
//如果此时根的值小于左右子树的最大值
//先交换元素的值
swap(arr,k,j);
//再更新k的值
k = j;
j = 2 * k + 1;
}else{
//此时根的值大于左右子树的最大值,不用交换
break;
}
}
}
//将两个节点的值交换
private static void swap(int[] arr,int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
🌈5. 归并排序
主要思想:先不断的将原数组一分为二,直到拆分后的子数组只剩下一个元素(归的体现),当数组只有一个元素时,天然有序。不断的将两个连续的有序子数组合并为一个大的数组,直到整个数组合并完成。
public static void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length-1);
}
/**
* 在arr[l,r)上进行归并排序
* @param arr
* @param l
* @param r
*/
private static void mergeSortInternal(int[] arr, int l, int r) {
//当前数组元素为0或者只有一个
if(l >= r){
return;
}
//防止索引越界mid = l+r/2
int mid = l + ((r-l) >>1);
//先将原数组一分为二,在子数组上进行归并排序
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid+1,r);
//此时两个子数组已经有序,将这两个子数组合并为一个大的有序数组
if(arr[mid] > arr[mid+1]){
merge(arr,l,mid,r);
}
}
/**
* 将两个子数组合并为原数组
* @param arr
* @param l
* @param mid
* @param r
*/
private static void merge(int[] arr, int l, int mid, int r) {
//创建一个大小为r-l+1的数组,该数组与原数组的长度一致,是临时数组
int[] aux = new int[r-l+1];
//将原数组的内容都拷贝过来:原数组名称,原数组的开始位置,目标数组的名称,目标数组的开始索引,需要拷贝的长度
System.arraycopy(arr,l,aux,0,r-l+1);
//两个子数组的开始位置
int i = l, j = mid +1;
//k表示当前原数组合并到哪个位置了
for (int k = l; k <= r; k++) {
//子数组1全部拷贝完毕,将子数组2的剩余内容写回arr
if(i > mid){
//创建的新数组下标从0开始,但是原数组的索引从l开始,差l的偏移量
arr[k] = aux[j-l];
j++;
} else if (j > r) {
arr[k] = aux[i-l];
i++;
} else if (aux[i-l] <= aux[j-l]) { //两个子数组都没走完,i小
//稳定性
arr[k] = aux[i-l];
i++;
} else{
arr[k] = aux[j-l];
j++;
}
}
}
问题:归并排序的应用:
海量数据处理。如果待排序的数据有100G,但是内存只有1G,需要借助磁盘将原数据等分为200份,每份数据大小500M,先将每份小文件加载到内存中,使用内部排序(快排或者归并)将这200个小文件排序(归并排序的子数组排序),最后进行200路归并,将200份文件写回原文件。(将200份文件的第一个值找出来,200个数据进行比较找出最小值写回,最小值对应的索引加加,其他的索引不变,然后再继续比较,直到200份文件的所有数据比较完。)
归并排序的时间复杂度:nlog(n) 归并排序的函数展开时间复杂度为logn,在参数k的for循环中,每遍历一次的复杂度是n。空间复杂度O(n)。对原始数据不敏感。
🌈6. 冒泡排序
核心思想:相邻的数据两两比较,小的放在左面,大的放在右面。在第一轮比较结束后,最大的元素已经放在了最后面。第二轮在剩余的元素中找最大值即可,第二轮可以少循环一次,后面依次类推。第二轮循环结束,次大值已经确定,然后第三循环继续在剩余数据中循环即可。
public static void main(String[] args) {
/**
* 冒泡排序:相邻的元素两两比较,大的放在右边,小的方左边。每比较完成一次,比较的元素中的最 大值已经放在了最右边。
*/
int[] arr = {2,4,5,3,1};
//注意这里的i的索引范围,不能越界
//外层for循环表示要执行多少轮
for (int i = 0; i < arr.length-1; i++) {
//内层for循环表示要比较的区间范围
for (int j = 0; j < arr.length-1-i; j++) {
if(arr[j] > arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
🌈7. 快速排序
核心思想:取数组的第一个值称为基准数,定义两个索引分别为start与end,分别表示数组的最开始索引和结束索引。end从后往前找第一个小于该基准数的数字,start从前I前向后找第一个大于基准数的数字,交换两者的位置,直到end和start指向同一个值,此时基准数归位,与start或end位置的值交换,此时第一次排序结束。我们可以达到的目标是原集合中所有小于该基准数的元素都在该基准数的左侧,大于该基准数的值都在右边。然后递归的在左半区间和右半区间不断重复该过程。
(1)使用递归方式:注意代码中的两点优化,在下述所有的代码中都可以使用。
public static void main(String[] args) {
int[] arr = {6,1,2,7,9,3,4,5,10,8};
quickSort(arr,0,9);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+ " ");
}
}
/**
*
* @param arr 我们要排序的数组
* @param i 要排序数组的开始索引
* @param j 要排序数组的结束索引
*/
private static void quickSort(int[] arr, int i, int j) {
//记录数组的开始和结束索引
int start = i;
int end = j;
// //递归的结束条件
if(start > end){
return;
}
/**
* 优化1:小数组直接使用插入排序:当待排序的数组元素特别多时
*/
// if(end-start <= 64){
// insetSort(arr);
// return;
// }
/**
* 优化2:每次取基准数的时候,取随机数。选取一个随机位置上的数字与start位置的数字交换
*/
// int randomIndex = ThreadLocalRandom.current().nextInt(start,end);
// System.out.println(randomIndex+"-----------");
// swap(arr,randomIndex,start);
//记录数组中的基准数:第一个元素
int baseNumber = arr[i];
//利用循环找到要交换的数字
while (start < end){
//end从后向前找,找到第一个小于基准数的数字
while (true){
if(end <= start || arr[end] < baseNumber){
break;
}
end--;
}
//start从前向后找,找到第一个大于基准数的数字
while (true){
if(end <= start || arr[start] > baseNumber){
break;
}
start++;
}
//不断的交换start与end索引下的值
swap(arr,start,end);
}
//此时start与end指向同一个值,循环结束。
//需要将基准数归位:将start或者end所指的数字(因为两个指向的其实是同一个数)与基准数(数组的第一个值)交换
swap(arr,start,i);
//第一轮排序已经结束,此时将基准数的两个左右区间排序即可:递归调用
//确定6左边的范围,重复刚才的过程
quickSort(arr,i,start-1);
//确定6右边的范围,重复刚才的过程
quickSort(arr,start+1,j);
}
//交换函数
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
(2)非递归方式:使用栈来实现。递归方式中的遍历看作二叉树的话是根左右,前序遍历,用栈实现。
/**
* 非递归方式快排:借助栈。递归方式中的遍历看作二叉树的话是根左右,前序遍历,用栈实现。层序遍历用队列。
* @param arr
*/
public static void quickSort(int[] arr){
//定义栈:先要弹出左边的l,因此先压右边的入栈
Deque<Integer> stack = new ArrayDeque<>();
stack.push(arr.length-1);
stack.push(0);
while (!stack.isEmpty()){
int l = stack.pop();
int r = stack.pop();
if(l >= r){
//当前这个子数组已经处理完毕,继续处理下一个子数组
continue;
}
int p = partitionByHole(arr,l,r);
//先将右半区间压入栈中
stack.push(r);
stack.push(p+1);
//将左半区间压入栈中
stack.push(p-1);
stack.push(l);
}
}
public static int partitionByHole(int[] arr, int l, int r) {
//分区点为最左侧元素
int baseNumbrt= arr[l];
int i = l, j = r;
while (i < j){
//让j从右开始一直找到第一个小于pivot的值
while (i < j && arr[j] > pivot){
j--;
}
//此时j所指的元素是第一个小于pivot的值,将值赋值给i索引上的值
arr[i] = arr[j];
//此时从i开始向右找第一个大于pivot的值,放在j索引位置上
while (i < j && arr[i] < pivot){
i++;
}
//i找到了第一个大于pivot的值
arr[j] = arr[i];
}
//此时i与j指向相同位置,回填分区点
arr[j] = baseNumbrt;
return j;
}
注意:快速排序在近乎有序的数组上的性能会退化?
由于基准数每次取得都是最左侧元素,若待排序元素(极端情况下)完全有序,则二叉树会退化为单支树,高度变为N。所以在选取元素时应尽可能的随机,保证当前基准数的左侧和右侧都有元素。
基准数的选择:
(1)三数取中法:每次从无序数组中的最左侧,最中间,最右侧位置选择这三个中间的元素作为基准数。
🎈(2)随机数法:每次从当前无序的数组中选择一个随机位置作为基准数。(代码中的优化2)
(3)分区方法2:《算法4》的分区
代码实现:
public static void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
/**
* 使用二路快排
* @param arr
* @param left 数组的最开始索引位置
* @param right 数组的最终索引位置
*/
private static void quickSortInternal(int[] arr, int left, int right) {
// base case
if(left >=right){
return;
}
int p = partition(arr,left,right);
quickSortInternal(arr,left,p-1);
quickSortInternal(arr,p+1,right);
}
private static int partition(int[] arr, int l, int r) {
//优化
int randomIndex = ThreadLocalRandom.current().nextInt(l,r);
swap(arr,l,randomIndex);
int v = arr[l];
//arr[l+1...j] < v 是小于v的区间
//arr[j+1,,,r] >= v 是大于等于v的区间
int j = l;//刚开始左半区间没有元素
//1、i不断从j之后的第一个元素开始遍历
for (int i = l+1; i <= r ; i++) {
//2、判断i索引扫描到的值 的大小情况
//如果该值大于v,i++;如果该值小于v,在执行相应操作后i还是要++继续遍历,所以这里只需要判断小于v的情况即可
if(arr[i] < v){
//交换大于等于v区间的第一个值与arr[i]的位置
swap(arr,i,j+1);
j++;
}
}
//3、将基准数v放在中间
swap(arr,l,j);
return j;
}
(4)三路快排
代码实现:
public static void quickSort3(int[] arr){
//调用内部方法
quickSortInternal3(arr,0,arr.length-1);
}
/**
* 三路快排:在一次操作中将所有重复元素一次放在最终位置
* 然后递归的在大于和小于基准数的自区间快排即可
* @param arr
* @param l 数组的开始索引位置
* @param r 数组的结束索引位置
*/
private static void quickSortInternal3(int[] arr, int l, int r) {
//base case
if(l >= r){
return;
}
//1、定义基准数v
// 随机交换基准数
int randomIndex = ThreadLocalRandom.current().nextInt(l,r);
swap(arr,randomIndex,l);
int v= arr[l];
//小于基准数的区间 arr[l+1...It]
//等于基准数的区间 arr[It+1,i]
//大于基准数的区间 arr[gt...r]
//初始化区间位置:区间刚开始都为空
int It = l;
int gt = r+1;
int i = It+1;
//2、遍历i索引位置的值,判断该值所属的区间
//终止条件:扫描的位置已经到大于v值的第一个值
while (i < gt){
if(arr[i] < v){
swap(arr,It+1,i);
It++; //更新区间范围
i++;
} else if (arr[i] > v) {
swap(arr,gt-1,i);
//更新区间范围
gt--;
//注意这里i不能直接++
} else{
i++;
}
}
//3、扫描完所有的元素,将基准数v放在等于v的区间内
swap(arr,l,It);
//4、递归的遍历左右区间
quickSortInternal3(arr,l,It-1);
quickSortInternal3(arr,gt,r);
}
🌾外部排序
线性排序: 桶排序 + 计数排序 + 基数排序
1、桶排序
将要排序的集合分散在若干个桶内(子数组)中,子数组的内部排序好,整个数组就有序了。几乎O(n)复杂度,对数据很敏感,只能在特定场景下使用(掌握原理)。
(1)要排序的数组要能均分在若干个桶内;
(2)桶和桶之间是相对有序的;
2、计数排序
是桶排序的特殊情况:只需要将原数组的所有元素扫描一遍后划分到不同的桶中,数据划分到不同的桶中后,桶内元素分组后不需要再排序。比如现在要按照年龄将所有的人排序,那么年龄相同的人就在
3、基数排序
基数排序最明显的特征是可以按位排序。如果最高位已经大于另一个元素,其他位数不需要再比较;如果最高位相同,则继续比较下一位。位与位之间是独立的。