2024年最新十大排序——最全最详细,一文让你彻底搞懂(1),热度飙升

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

代码实现 C++

vector<int> insertionSort(vector<int>& arr) {
    int len = arr.size();
    int preIndex, current;
    for(int i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

👆 Top

希尔排序

Shell Sort

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

相对于简单的插入排序,希尔排序的设计更多地考虑了数据本身的特征。本质上来说,希尔排序也是插入排序,或者说简单插入排序是gap = 1的希尔排序(gap下面代码中会提及,就是增量的意思)。比如对于543210的处理,我们想要让其变成递增序列。如果是传统的插入排序,那么需要比较和移动很多次,相当麻烦。但如果是希尔排序,变化过程是:543210 -> 210543 -> 012345 。很快就得到了答案。543210这串数字是有规律的数字,规律就是递减。实际处理数据的时候,数据本身很可能带有某种规律。简单插入排序是“埋头干”,希尔排序有参考到数据本身的性质,因此效率更高。

算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 1.选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 2.按增量序列个数k,对序列进行k 趟排序;
  • 3.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

动图演示

图示算法

代码实现 C++

    void shellSort(vector<int>& arr) {
        // 增量gap,并逐步缩小增量
       for (int gap = arr.size() / 2; gap > 0; gap /= 2) {
           // 从第gap个元素,逐个对其所在组进行直接插入排序操作
           for (int i = gap; i < arr.size(); i++){
               int j = i;
               while (j - gap >= 0 && arr[j] < arr[j-gap]) {           
                   swap(arr[j], arr[j - gap]);
                   j -= gap;
               }
           }
       }
    }

代码实现 Java

package sortdemo;
import java.util.Arrays;

public class ShellSort {
    public static void main(String []args) {
        int []arr ={1,4,2,7,9,8,3,6};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void sort(int []arr) {
        // 增量gap,并逐步缩小增量
       for(int gap = arr.length/2; gap > 0; gap /= 2) {
           // 从第gap个元素,逐个对其所在组进行直接插入排序操作
           for (int i = gap; i < arr.length; i++){
               int j = i;
               while (j - gap >= 0 && arr[j] < arr[j-gap]) {           
                   swap(arr, j, j - gap);
                   j -= gap;
               }
           }
       }
    }

    public static void swap(int []arr, int a, int b){ // 这里交换次序和传统的int temp的方法是一样的!思路比较特别。
        arr[a] = arr[a] + arr[b];
        arr[b] = arr[a] - arr[b];
        arr[a] = arr[a] - arr[b];
    }
}

参考链接:图解排序算法(二)之希尔排序

👆 Top

选择排序
简单选择排序

Selection Sort

一般来说,选择排序就指代简单选择排序。

选择排序是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,
然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 1.初始状态:无序区为R[1…n],有序区为空;
  • 2.第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • 3.n-1趟结束,数组有序化了。

动图演示

代码实现 C++

vector<int> selectionSort(vector<int>& arr) {
    int len = arr.size();
    int minIndex, temp;
    for(int i = 0; i < len - 1; i++) {
        minIndex = i;
        for(int j = i + 1; j < len; j++) {
            if(arr[j] < arr[minIndex]) {    // 寻找最小的数
                minIndex = j;                // 将最小数的索引保存
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
} 

👆 Top

堆排序

Heap Sort

堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

除了叫做堆,很多地方也叫作优先队列(priority queue)。因此,在调用一些函数或者使用STL的时候,记得看到优先队列,就知道是堆这样的结构。

下面的图是大根堆:(最大值在树的根部)

下面是小根堆:(最小值在树根)

看到这里,可以发现,堆这样的结构和二叉搜索树(Binary Search Tree)很像。所以这里,要对比一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z0tzvire-1616229982936)(https://media.geeksforgeeks.org/wp-content/uploads/Untitled-Diagram-2-7.png)]

上图是二叉搜索树。很直观的不同在于:

  1. 二叉搜索树不是完全二叉树,但是堆是一个近似完全二叉树的结构;
  2. 二叉搜索树的根节点大于左节点,小于右节点。但是堆的根节点是大于或者小于左节点与右节点的。

什么是完全二叉树?

算法描述

  • 1.将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

动图演示

代码实现 C++

class Solution {
public:
    void heapify (vector<int>& arr, int index, int len) { // 建堆
        int left = 2 \* index + 1; // 递归方式构建大根堆(len是arr的长度,index是第一个非叶子节点的下标)
        int right = 2 \* index + 2; // 根据堆的结构,可知有这样的left和right数值
        int maxIndex = index; // 因为是大根堆,所以关注max
        if ((left < len) && (arr[maxIndex] < arr[left])) { // 两个if判断,为了把三个节点的最大值找到。同时left和right不可超出索引
            maxIndex = left;
        }
        if ((right < len) && (arr[maxIndex] < arr[right])) {
            maxIndex = right;
        } 
        if (maxIndex != index) { // 如果有变动了
            swap(arr[maxIndex], arr[index]); // 那就把数值进行交换。这边的交换就是堆结构中节点的交换。把大的数值放在上面,小的当子节点。
            heapify(arr, maxIndex, len); // 递归调用,继续让该上浮的元素上浮
        }
    }
    void heapSort (vector<int>& arr, int size) { // 排序
        for (int i = size / 2 - 1; i >=0; i--) { // 从尾巴开始。倒序的原因和堆的结构与我们的定义有关。堆的顶端是我们要的,那个数值是
        // 经过全局比较得到的最大值或最小值。这样一来,最小的索引对应全局的比较,那么我们需要倒序的方式建堆。
            heapify(arr, i, size);
        }
        for (int i = size - 1; i >= 1; i--) { // 将最大的数值放在数组的末尾。堆排序的过程是弹出最大值。我们每次都把最大值放最后,
        // 得到的效果就是递增数组。
            swap(arr[0], arr[i]); // 将弹出的最大值放最后
            heapify(arr, 0, i); // 接下来把弹出的值排除在外,对堆的内部进行移动,得到新的弹出的最大值。
        // 这一段移动的代码还是很巧妙的 
        }
    }
    vector<int> sortArray(vector<int>& nums) {
        heapSort(nums, nums.size());
        return nums;
    }
};

代码实现 Python

def heapify(arr,n,i): # 建堆
    # n = len(arr) heap construction
    leftchild = 2\*i + 1 # i starts with zero in python
    rightchild = 2\*i + 2 # left和right的数值源于堆的结构
    
    largest = i
    # 大顶堆,因此关心最大值
    if leftchild < n and arr[i] < arr[leftchild]:
        largest = leftchild
    if rightchild < n and arr[largest] < arr[rightchild]: # a little bit tricky
        largest = rightchild
    if i != largest: # 如果largest变动了
        arr[i], arr[largest] = arr[largest], arr[i] # 交换位置,最大的浮上去
        heapify(arr,n,largest) # 递归调用,继续移动 
    return arr


def heapsort(arr):
    n = len(arr)
    for i in range(n-1,-1,-1): # 倒序建堆,堆的结构决定循环顺序是倒的
        heapify(arr,n,i)
    for i in range(n-1,0,-1): # 把最大值移动到最后面
        arr[0], arr[i] = arr[i], arr[0]
        heapify(arr,i,0) # 排除最大值在外,继续堆排序,弹出最大值
    return arr  # 得到一个递增数组
    
arr = list(map(int,input().split()))
arr2 = heapsort(arr)
for i in range(0,len(arr2)):
    print(arr2[i],end = ' ')
#print(heapsort(arr)) # print a list not numbers

Python的思路和C++相仿。

参考链接:【排序】堆排序,C++实现

相关力扣题目

heap经常用于类似于Top K出现频率之类的题目。heap使用的时候,要分清小根堆还是大根堆。如果留的是最高的K个频次,那么是小根堆。因为要把小的踢出heap,留大的,所以是小根堆,大的留住,小的数值浮到根上踢出。

关于Top K问题,推荐阅读材料:拜托,面试别再问我TopK了!!!

在C++中,堆的使用是:priority_queue heap; 函数,关于这个函数的使用(大根堆还是小根堆,入堆,出堆,元素访问…),见下面的内容:

priority_queue<int> heap; // 生成一个大根堆
heap.push(element); // 增加一个元素到堆
heap.top();  // 访问堆的根部,最大值或是最小值
heap.pop();  // 删除根部的元素,最大值或者是最小值
// 更多细节详见LC1046, 这可以是一道简单的堆使用的模板题

LC912_Sort an Array 排序数组 堆排序模板,就是上面C++代码的内容。

LC347 最高频次的K个数

LC215 数组中的第K个最大元素

LC1046 Last Stone Weight 最后一块石头的重量
思路:对这些石头处理,需要排序;但是可能两块石头重量不相等,粉碎之后还需要加入数组。如果每次遇到这种情况,都用sort函数不方便。
有什么东西可以要用的时候方便拿,不用的时候直接丢进去?只能是堆了,所以堆排序。

LC239 Sliding Window Maximum 滑动窗口最大值
本题也可以自己制造数据结构求解单调队列,但是用堆更简单。
我们在意的是最大值,所以用堆弹出最大值,然后让窗口持续移动即可。要小心的是大顶堆的最大值或者堆内部的一些数值已经不在窗口之中,这一点要注意维护。

👆 Top

归并排序
二路归并排序

Merge Sort

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用 分治法(Divide and Conquer) 的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

一般说到的归并排序就是这种二路归并。

算法描述

  • 1.把长度为n的输入序列分成两个长度为n/2的子序列;
  • 2.对这两个子序列分别采用归并排序;
  • 3.将两个排序好的子序列合并成一个最终的排序序列。

动图演示

图示算法

代码实现 Python

(首先使用Python的原因在于:C++或者其他语言书写较为繁琐,归并排序的思想使用Python语言就可以简洁明晰地表达。)


def mergesort(seq):
    if len(seq) <= 1:
        return seq
    mid = len(seq)//2  
    
    left = mergesort(seq[:mid])  # divide 分割
    right = mergesort(seq[mid:])
    
    return merge(left, right) # merge 合并

def merge(left, right):
    result = []  
    i = 0  
    j = 0
    
    while i < len(left) and j < len(right): 
        if left[i] <= right[j]: # 比较大小,排序
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result += left[i:]  # 这两行的处理,是防止left或者right的一边处理完毕,另一边还没有处理完毕。把剩余部分直接放入result中。
    result += right[j:]
    return result

# test the code
seq = [5,3,0,6,1,4]
print(seq)
result = mergesort(seq)
print(result)


对照上面的Python代码,下面也给出C++代码:

#include <cstdio>
#include <iostream>
 
using namespace std;
 
// Function to Merge Arrays L and R into A.
// lefCount = number of elements in L
// rightCount = number of elements in R.
void Merge(int \*A,int \*L,int leftCount,int \*R,int rightCount) {
	int i,j,k;
 
	// i - to mark the index of left aubarray (L)
	// j - to mark the index of right sub-raay (R)
	// k - to mark the index of merged subarray (A)
	i = 0; j = 0; k =0;
 
	while(i<leftCount && j< rightCount) {
		if(L[i]  < R[j]) A[k++] = L[i++];
		else A[k++] = R[j++];
	}
	while(i < leftCount) A[k++] = L[i++];
	while(j < rightCount) A[k++] = R[j++];
}
 
// Recursive function to sort an array of integers.
void MergeSort(int \*A,int n) {
	int mid,i, \*L, \*R;
	if(n < 2) return; // base condition. If the array has less than two element, do nothing.
 
	mid = n/2;  // find the mid index.
 
	// create left and right subarrays
	// mid elements (from index 0 till mid-1) should be part of left sub-array
	// and (n-mid) elements (from mid to n-1) will be part of right sub-array
	L = new int[mid];
	R = new int [n - mid];
 
	for(i = 0;i<mid;i++) L[i] = A[i]; // creating left subarray
	for(i = mid;i<n;i++) R[i-mid] = A[i]; // creating right subarray
 
	MergeSort(L,mid);  // sorting the left subarray
	MergeSort(R,n-mid);  // sorting the right subarray
	Merge(A,L,mid,R,n-mid);  // Merging L and R into A as sorted list.
	// the delete operations is very important
	delete [] R;
	delete [] L;
}
 
int main() {
	/\* Code to test the MergeSort function. \*/
 
	int A[] = {6,2,3,1,9,10,15,13,12,17}; // creating an array of integers.
	int i,numberOfElements;
 
	// finding number of elements in array as size of complete array in bytes divided by size of integer in bytes.
	// This won't work if array is passed to the function because array
	// is always passed by reference through a pointer. So sizeOf function will give size of pointer and not the array.
	// Watch this video to understand this concept - http://www.youtube.com/watch?v=CpjVucvAc3g
	numberOfElements = sizeof(A)/sizeof(A[0]);
 
	// Calling merge sort to sort the array.
	MergeSort(A,numberOfElements);
 
	//printing all elements in the array once its sorted.
	for(i = 0;i < numberOfElements;i++)
	   cout << " " << A[i];
	return 0;
}

C++代码的参考链接:C++ Merge sort(归并排序)

看了Python代码再看C++代码,是不是感觉更简单了呢?

相关力扣题目

LC148 对链表进行排序

多路归并排序

很难碰到这种排序问题,所以这里就不讨论了。感兴趣的同学可以自行搜索。

👆 Top

非比较排序
计数排序

Counting Sort

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

算法描述

  • 1.找出待排序的数组中最大和最小的元素;
  • 2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

动图演示

代码实现 C++

#include <iostream>
#include <vector>
using namespace std;
 
void countingSort(vector<int> &arr, int maxVal) {
	int len = arr.size();
	if (len < 1) return;
	vector<int> count(maxVal+1, 0);
	vector<int> tmp(arr); // 把原数组备份,后面反向填充的时候要用到
	for (auto x : arr)
		count[x]++;
	for (int i = 1; i <= maxVal; ++i)
		count[i] += count[i - 1];
	for (int i = len - 1; i >= 0; i--) { // 反向填充目标数组
		arr[count[tmp[i]] - 1] = tmp[i]; // 进行排序
		count[tmp[i]]--;    // 排好了一个元素,这边需要减一
	}
}
 
int main()
{
	vector<int> arr = { 1,5,3,7,6,2,8,9,4,3,3 };
	int maxVal = 9;
	countingSort(arr,maxVal);
	for (auto x : arr)
		cout << x << " ";
	cout << endl;
	return 0;
}

参考链接:C++排序算法之计数排序

分析

计数排序是一个稳定的排序算法。当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

此外,很明显可以看到:计数排序局限于正整数。如果要对其他类型的数值进行排序,应该要先进行预处理。

👆 Top

基数排序

Radix Sort

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

基数排序按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

算法描述

  • 1.取得数组中的最大数,并取得位数;
  • 2.arr为原始数组,从最低位开始取每个位组成radix数组;
  • 3.对radix进行计数排序(利用计数排序适用于小范围数的特点)。

动图演示

代码实现 C++

int maxbit(int data[], int n) //辅助函数,求数据的最大位数
{
    int maxData = data[0];              // 最大数
    // 先求出最大数,再求其位数,这样有原先依次每个数判断其位数,稍微优化点。
    for (int i = 1; i < n; ++i)
    {
        if (maxData < data[i])
            maxData = data[i];
    }
    int d = 1;
    int p = 10;
    while (maxData >= p)
    {
        //p \*= 10; // Maybe overflow
        maxData /= 10;
        ++d;
    }
    return d;
/\* int d = 1; //保存最大的位数
 int p = 10;
 for(int i = 0; i < n; ++i)
 {
 while(data[i] >= p)
 {
 p \*= 10;
 ++d;
 }
 }
 return d;\*/
}
void radixsort(int data[], int n) //基数排序
{
    int d = maxbit(data, n);
    int \*tmp = new int[n];
    int \*count = new int[10]; //计数器
    int i, j, k;
    int radix = 1;
    for(i = 1; i <= d; i++) //进行d次排序
    {
        for(j = 0; j < 10; j++)
            count[j] = 0; //每次分配前清空计数器
        for(j = 0; j < n; j++)
        {
            k = (data[j] / radix) % 10; //统计每个桶中的记录数
            count[k]++;
        }
        for(j = 1; j < 10; j++)
            count[j] = count[j - 1] + count[j]; //将tmp中的位置依次分配给每个桶
        for(j = n - 1; j >= 0; j--) //将所有桶中记录依次收集到tmp中
        {
            k = (data[j] / radix) % 10;
            tmp[count[k] - 1] = data[j];
            count[k]--;
        }
        for(j = 0; j < n; j++) //将临时数组的内容复制到data中
            data[j] = tmp[j];
        radix = radix \* 10;
    }
    delete []tmp;
    delete []count;
}

参考链接:基数排序

这个排序算法比较少遇到,大致看看思路就好。

👆 Top

桶排序

Bucket Sort

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量;
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

根据上面的定义,我们来探究一下:

1.什么时候排序最慢?

当输入的数据被分配到了同一个桶中。

2.什么时候排序最快?

当输入的数据可以均匀的分配到每一个桶中。

算法描述

  • 1.设置一个定量的数组当作空桶;
  • 2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 3.对每个不是空的桶进行排序;
  • 4.从不是空的桶里把排好序的数据拼接起来。

图示算法

在桶中的元素分布:

然后,元素在每个桶中排序:

关于桶排序的动态图,可以点击这里

代码实现 C++

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

被分配到了同一个桶中。

2.什么时候排序最快?

当输入的数据可以均匀的分配到每一个桶中。

算法描述

  • 1.设置一个定量的数组当作空桶;
  • 2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 3.对每个不是空的桶进行排序;
  • 4.从不是空的桶里把排好序的数据拼接起来。

图示算法

在桶中的元素分布:

然后,元素在每个桶中排序:

关于桶排序的动态图,可以点击这里

代码实现 C++

[外链图片转存中…(img-U3psCisf-1715750086465)]
[外链图片转存中…(img-at4HIlQS-1715750086466)]
[外链图片转存中…(img-55vHyAZL-1715750086466)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值