重温数据结构与算法之排序算法可视化

排序算法

前言

  1. 定义

    根据百度百科

    所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。而排序算法,就是如何使得记录按照要求排列的方法。

    和维基百科

    在计算机科学中,排序算法是将列表元素按顺序排列的算法。最常用的顺序是数字顺序和字典顺序,以及升序或降序。

    可以看出排序算法就是将一组元素以递增或递减的顺序重新排列的方法

  2. 作用

    1. 方便查找

      在一个无序的数组中查找需要 O ( n ) O(n) O(n)的时间,而在一个有序的数组中使用二分法查找只需要 O ( l o g n ) O(logn) O(logn)时间。你可能不会想到这其中的差异,举个栗子:一个数组中有 2 32 2^{32} 232 个数,无序数组查找平均需要21亿次,而有序数组只需要32次

    2. 计算机执行效率更快

      stackoverflow上有一个高赞问题: why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array,相同的代码,先排序后计算比不排序直接计算快4-5倍。

      根据高赞回答,可以看到这是因为计算机处理器的缘故,在内部有分支预测器,排序的数组被优化执行速率更快。

      在计算机体系结构中,分支预测器是一种数字电路,它试图猜测支路(例如if-then-else结构)在最终已知之前将走哪条路。分支预测器的目的是改善指令管道中的流量。在许多现代流水线微处理器架构(如x86)中,分支预测器在实现高性能方面发挥着关键作用

  3. 分类

    根据排序的特点可以根据以下特征进行分类

    算法名称最好平均最坏内存是否稳定是否比较备注
    冒泡排序(Bubble sort) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) 1 1 1
    选择排序(Selection sort) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) 1 1 1
    插入排序(Insertion sort) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) 1 1 1
    希尔排序(Shellsort) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 4 / 3 ) O(n^{4/3}) O(n4/3) O ( n 3 / 2 ) O(n^{3/2}) O(n3/2) 1 1 1
    归并排序(Merge sort) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) n n n
    快速排序(Quicksort) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n 2 ) O(n^2) O(n2) l o g n logn logn
    堆排序(Heapsort) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) 1 1 1
    计数排序(Counting sort) O ( n + r ) O(n+r) O(n+r) O ( n + r ) O(n+r) O(n+r) O ( n + r ) O(n+r) O(n+r) n + r n+r n+rr 排序数字的范围
    桶排序(Bucket sort) O ( n + r ) O(n+r) O(n+r) O ( n + r ) O(n+r) O(n+r) O ( n 2 ) O(n^2) O(n2) n + r n+r n+rr 排序数字的范围
    基数排序(LSD Radix Sort) O ( n ) O(n) O(n) O ( n ∗ k d ) O(n*\frac{k}{d}) O(ndk) O ( n ∗ k d ) O(n*\frac{k}{d}) O(ndk) O ( n + 2 d ) O(n+2^d) O(n+2d)d:数字位数,k:数字的大小
    基数排序(MSD Radix Sort) O ( n ∗ k d ) O(n*\frac{k}{d}) O(ndk) O ( n ∗ k d ) O(n*\frac{k}{d}) O(ndk) O ( n ∗ k d ) O(n*\frac{k}{d}) O(ndk) O ( n + 2 d ) O(n+2^d) O(n+2d)d:数字位数,k:数字的大小
    Tim排序(Timsort) O ( n ) O(n) O(n) O ( n l o g n ) O(nlogn) O(nlogn) O ( n l o g n ) O(nlogn) O(nlogn) n n njava和python内置的排序算法

最好,平均,最坏:指时间复杂度

内存:指需要的额外内存

稳定:指key相等的元素保持和未排序之前的相对顺序

比较:指列表内的记录比较大小

一、冒泡排序

在这里插入图片描述

冒泡排序,每相邻的两个元素比较,在升序中,会依次将大的元素交换到末尾

void bubbleSort(int *nums, int numsSize)
{
    for (int i = 0; i < numsSize - 1; i++)
        for (int j = 0; j < numsSize - 1 - i; j++)
        {
            if (nums[j] > nums[j + 1])
                swap(nums, j , j + 1);
        }
}
void swap(int *nums, int i, int j)
{
    int tmp = nums[i];
    nums[i] = nums[j];
    nums[j] = tmp;
}
function bubbleSort(nums, numsSize) {
    for (let i = 0; i < numsSize - 1; i++) {
        for (let j = 0; j < numsSize - 1 - i; j++) {
            if (nums[j] > nums[j + 1]) {
                [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
            }       
        }
    }
}

二、选择排序

在这里插入图片描述

选择排序,选择最大元素的索引与最后交换

void selectSort(int *nums, int numsSize)
{
    for (int i = 0; i < numsSize - 1; i++)
    {
        int max = 0;
        for (int j = 0; j < numsSize - i; j++)
        {
            if (nums[j] > nums[max]) max = j;
        }
        swap(nums, numsSize - i - 1, max);
    }
}
function selectSort(nums, numsSize) {
     for (let i = 0; i < numsSize - 1; i++){
         let max = 0;
         for (let j = 0; j < numsSize - i; j++){
             if (nums[j] > nums[max]) max = j;
         }
         [nums[numsSize - i - 1], nums[max]] = [nums[max], nums[numsSize - i - 1]];
     }  
}

三、插入排序

在这里插入图片描述

插入排序,顾名思义,将前面的序列看成有序的序列,将大于要插入值的数据后移,然后将值插进去

void insertSort(int *nums, int numsSize)
{
    for (int i = 0; i < numsSize; i++)
    {
        int tmp = nums[i];
        int j = i;
        for (; j > 0 && nums[j - 1] > tmp; j--)
            nums[j] = nums[j - 1];
        nums[j] = tmp;
    }
}
function insertSort(nums, numsSize) {
    for (let i = 0; i < numsSize; i++)
    {
        let tmp = nums[i];
        let j = i;
        for (; j > 0 && nums[j - 1] > tmp; j--) {
            nums[j] = nums[j - 1];
        }
        nums[j] = tmp;
    }
}

四、希尔排序

在这里插入图片描述

希尔排序又称缩小增量排序,因 DL.Shell 于 1959 年提出而得名。它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。

void shellSort(int *nums, int numsSize) {
 	for (int k = numsSize / 2; k > 0; k /= 2) {
        for (int i = k; i < numsSize; i++) {
            for (int j = i; j >= k; j -= k) {
                if (nums[j] < nums[j - k]) {
                    swap(nums, j, j - k);
                }
            }
        }
    }  
}
function shellSort(nums, numsSize){
	for (let k = Math.floor(numsSize / 2); k > 0; k = Math.floor(k/2)) {
        for (let i = k; i < numsSize; i++) {
            for (let j = i; j >= k; j = j - k) {
                if (nums[j] < nums[j - k]) {
                    [nums[j], nums[j - k]] = [nums[j - k], nums[j]];    
                }
            }
            
        }
    } 
}

五、归并排序

归并排序

归并排序,是利用分治,将一个乱序的整体,递归分治为可以比较小的有序部分,最后递归合并成一个大的有序整体

void mergeSort(int *nums, int start, int end) {
    if (end == start) return;
    int mid = (start + end) / 2;
    mergeSort(nums, start, mid);
    mergeSort(nums, mid + 1, end);
    merge(nums, start, mid, end);
}

void merge(int *nums, int start, int mid, int end) {
    int *tmp = malloc((end - start + 1) * sizeof(int));
    int i = start, j = mid + 1, k = 0;
    while (i <= mid || j <= end) {
        while (i <= mid && (j > end || nums[i] <= nums[j]))
            tmp[k++] = nums[i++];
        while (j <= end && (i > mid || nums[j] <= nums[i]))
            tmp[k++] = nums[j++];
    }
    while (k--) {
        nums[start + k] = tmp[k];
    }
    free(tmp);
}
function mergeSort(nums, start, end) {
	if (end == start) return;
	let mid = Math.floor((start + end) / 2);
	mergeSort(nums, start, mid);
	mergeSort(nums, mid + 1, end);
	merge(nums, start, mid, end);
}

function merge(nums, start, mid, end) {
    let tmp = new Array;
    let i = start, j = mid + 1, k = 0;
    while (i <= mid || j <= end) {
        while (i <= mid && (j > end || nums[i] <= nums[j])) {
            tmp[k++] = nums[i++];
        }
        while (j <= end && (i > mid || nums[j] <= nums[i])) {
            tmp[k++] = nums[j++];
        }
    }
    while (k--) {
        nums[start + k] = tmp[k];
    }
}

六、快速排序

快速排序

快速排序,利用到分治,双指针,选择一个基准点key,一般是数组的第一个数,然后用left和right两个值,指向要排序数组的首尾,然后依次从left找到比key大的,从right找到比key小的,然后交换,保证在left和right相等前,左边的都比key小,右边的都比key大,最后,由于left==right时退出,需要知道nums[left]是否大于key,大于则将前一位与所在与key所在start交换

void quickSort(int *nums, int start, int end) 
{ 
	if (start >= end) return;
    int key = nums[start];
    int left = start;
    int right = end;
    while(left < right)
    {
        while (left < right && nums[left] <= key)
            left++;
        while (left < right && nums[right] >= key)
            right--;
        swap(nums, left, right);
    }

    if (nums[left] <= key)
        swap(nums, start, left);
    else
        swap(nums, start, --left);
    quickSort(nums, start, left - 1);
    quickSort(nums, left + 1, end);
}
function quickSort(nums, start, end) { 
        if (start >= end) return;
        let key = nums[start];
        let left = start;
        let right = end;
        while(left < right)
        {
            while (left < right && nums[left] <= key)
                left++;
            while (left < right && nums[right] >= key)
                right--;
            [nums[left], nums[right]] = [nums[right], nums[left]];
        }

        if (nums[left] <= key){
            [nums[start], nums[left]] = [nums[left], nums[start]];
        }
        else{
            left--;
            [nums[start], nums[left]] = [nums[left], nums[start]];
        }
        quickSort(nums, start, left - 1);
        quickSort(nums, left + 1, end);
}

七、堆排序

在这里插入图片描述

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

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆可以用数组表示, 根节点存储在索引0处;如果i是当前节点的索引,则
i P a r e n t ( i ) = f l o o r ( ( i − 1 ) / 2 ) i L e f t C h i l d ( i ) = 2 ∗ i + 1 i R i g h t C h i l d ( i ) = 2 ∗ i + 2 iParent(i) = floor((i-1) / 2) \\ iLeftChild(i) = 2*i + 1\\ iRightChild(i) = 2*i + 2 iParent(i)=floor((i1)/2)iLeftChild(i)=2i+1iRightChild(i)=2i+2

堆排序有两种实现:heapify 和 shiftDown 递归,迭代

void heapSort(int *nums, int numsSize) {
    
     for (int i = numsSize / 2; i >= 0; i--) {
        heapify(nums, i, numsSize);
    }

    for (int i = numsSize - 1; i > 0; i--) {
        swap(nums, 0, i);
        heapify(nums, 0, i);
    }

}

void heapify(int * nums, int i, int numsSize) {     // 堆调整
    int left = 2 * i + 1, right = 2 * i + 2, parent = i;

    if (left < numsSize && nums[left] > nums[parent]) {
        parent = left;
    }

    if (right < numsSize && nums[right] > nums[parent]) {
        parent = right;
    }

    if (parent != i) {
        swap(nums, i, parent);
        heapify(nums, parent, numsSize);
    }
}
void heapSort(int *nums, int numsSize) {
    for (int i = numsSize / 2; i >= 0; i--) {
        shiftDown(nums, numsSize, i);
    }

    for (int i = numsSize - 1; i > 0; i--) {
        swap(nums, 0, i);
        shiftDown(nums, 0, i);
    }
}

void shiftDown(int * nums, int k, int numsSize) { 
    int leftChild = 2 * k + 1, rightChild = 2 * k + 2;
    while (leftChild < numsSize) {
        int biggerChild = leftChild;
        if (rightChild < numsSize && nums[rightChild] > nums[leftChild]) {
            biggerChild = rightChild;
        }
        if (nums[k] > nums[biggerChild]) break;
        swap(nums, k, biggerChild);
        k = biggerChild;
        leftChild = 2 * k + 1, rightChild = 2 * k + 2;
    }
}
function heapSort(nums, numsSize) {
    for (let i = Math.floor(numsSize / 2); i >= 0; i--) {
        shiftDown(nums, i, numsSize);
    }

    for (let i = numsSize - 1; i > 0; i--) {
        [nums[0], nums[i]] = [nums[i], nums[0]];
        shiftDown(nums, 0, i);
    }
}

function shiftDown(nums, k, numsSize) { 
    let leftChild = 2 * k + 1, rightChild = 2 * k + 2;
    while (leftChild < numsSize) {
        let biggerChild = leftChild;
        if (rightChild < numsSize && nums[rightChild] > nums[leftChild]) {
            biggerChild = rightChild;
        }
        if (nums[k] > nums[biggerChild]) break;
        
        [nums[k], nums[biggerChild]] = [nums[biggerChild], nums[k]];
        k = biggerChild;
        leftChild = 2 * k + 1, rightChild = 2 * k + 2;
    }
}

八、计数排序

在这里插入图片描述

计数排序是一种特殊的桶排序,数字范围在r(r不能太大)的数组排序,例如0-100分的排序

void countingSort(int * nums,int numsSize) {
    int max = 0x80000000;
    int min = 0x7fffffff;
    for (int i = 0; i < numsSize; i++) {
        max = nums[i] > max ? nums[i] : max;
        min = nums[i] < min ? nums[i] : min;
    }
    int r = max - min + 1;
    int *tmp = (int *)malloc(r * sizeof(int));
    memset(tmp, 0, r * sizeof(int));

    for (int i = 0; i< numsSize; i++) {
        tmp[nums[i] - min]++;
    }

    int index = 0;
    for (int j = 0; j< r; j++) {
        while (tmp[j]) {
            nums[index++] = j + min;
            tmp[j]--;
        }
    }
    free(tmp);
    tmp = NULL;
}
function countingSort(nums, numsSize) {
    let max = nums[0];
    let min = nums[0];
    for (let i = 1; i < numsSize; i++) {
        max = Math.max(max, nums[i]);
        min = Math.min(min, nums[i]);
    }
    let r = max - min + 1;
    let tmp = Array(r).fill(0);
 
    for (let i = 0; i< numsSize; i++) {
        tmp[nums[i] - min]++;
    }

    let index = 0;
    for (let j = 0; j< r; j++) {
        while (tmp[j] > 0) {
            nums[index++] = j + min;
            tmp[j]--;
        }
    }
}

九、桶排序

桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

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

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

  • 什么时候最快

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

  • 什么时候最慢

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

void bucketSort(int * nums, int numsSize)
{
    int max = 0x80000000;
    int min = 0x7fffffff;
    int bucketNum = 5;
    for (int i = 0; i < numsSize; i++) {
        max = nums[i] > max ? nums[i] : max;
        min = nums[i] < min ? nums[i] : min;
    }

    int bucketSize = (max - min + 1) / bucketNum + 1;
    int ** buckets = (int **)malloc(bucketNum * sizeof(int *));
    int * curBucketsSize = (int *)calloc(bucketNum, sizeof(int));


    for (int i = 0; i < bucketNum; i++)
    {
        buckets[i] = (int *)malloc(bucketSize * 1.5 * sizeof(int));
    }

    int index = 0;
    for (int i = 0; i< numsSize; i++) {
        index = (nums[i] - min) / bucketSize;

        buckets[index][curBucketsSize[index]] = nums[i];
	
        if (curBucketsSize[index] > bucketSize) {
            //扩容
            int *tmp = (int *) realloc(buckets[index], curBucketsSize[index] * 1.5 * sizeof(int));
            if (tmp == NULL) {
                printf("something error, realloc memory fail");
                exit(-1);
            } else {
                buckets[index] = tmp;
            }
        }
        curBucketsSize[index]++;
    }

    index = 0;
    for (int i = 0; i < bucketNum; i++) {
        insertSort(buckets[i], curBucketsSize[i]);
        for (int j = 0; j < curBucketsSize[i]; j++) {
            nums[index++] = *(buckets[i] + j);
        }
        free(buckets[i]);
    }
    free(buckets);
    free(curBucketsSize);
}
function bucketSort(nums, numsSize){
    let max = nums[0];
    let min = nums[0];
    let bucketNum = 5;
    for (let i = 0; i < numsSize; i++) {
        max = nums[i] > max ? nums[i] : max;
        min = nums[i] < min ? nums[i] : min;
    }

    let bucketSize = Math.floor((max - min + 1) / bucketNum) + 1;
    let buckets = new Array(bucketNum);
    for (var i = buckets.length - 1; i >= 0; i--) {
        buckets[i] = new Array; 
    }


    let index = 0;
    for (let i = 0; i< numsSize; i++) {
        index = Math.floor((nums[i] - min) / bucketSize);
        buckets[index].push(nums[i]);
    }

    index = 0;
    for (let i = 0; i < bucketNum; i++) {
        insertSort(buckets[i], buckets[i].length);
        for (let j = 0; j < buckets[i].length; j++) {
            nums[index++] = buckets[i][j];  
        }     
    }    
}

十、基数排序

在这里插入图片描述

一组正整数,可以先按个位的大小对所有数进行排序,然后再按十位进行排序,一直到最高位,这样就可以使整组数据变得有效,这样从最低位开始的方法称为最低位优先LSD(Least Significant Digit first), 反之从最高位开始,最后再比较最低位,则称之为最高位优先MSD(Most Significant Digit first)

基数排序有以上两种

#define BASE 10
void radixSort(int * nums, int numsSize) {

    int *tmp, max = nums[0], exp = 1;

    tmp = calloc(numsSize, sizeof(int));

    for (int i = 1; i < numsSize; i++) {
        if (nums[i] > max) {
            max = nums[i];
        }
    }

    while (max / exp > 0) {
        int bucket[BASE] = { 0 };

        for (int i = 0; i < numsSize; i++) {
          bucket[(nums[i] / exp) % BASE]++;
        }

        for (int i = 1; i < BASE; i++) {
          bucket[i] += bucket[i - 1];
        }

        for (int i = numsSize - 1; i >= 0; i--) {
          tmp[--bucket[(nums[i] / exp) % BASE]] = nums[i];
        }

        for (int i = 0; i < numsSize; i++) {
          nums[i] = tmp[i];
        }
        exp *= BASE;
    }
}
function radixSort(nums, numsSize) {

    let tmp = new Array(numsSize), max = nums[0], exp = 1, base = 10;
 
    for (let i = 1; i < numsSize; i++) {
        if (nums[i] > max) {
            max = nums[i];
        }
    }

    while (max / exp > 0) {
        let bucket = Array(base).fill(0);

        for (let i = 0; i < numsSize; i++) {
          bucket[(Math.floor(nums[i] / exp)) % base]++;
        }

        for (let i = 1; i < base; i++) {
          bucket[i] += bucket[i - 1];
        }

        for (let i = numsSize - 1; i >= 0; i--) {
          tmp[--bucket[Math.floor(nums[i] / exp) % base]] = nums[i];
        }

        for (let i = 0; i < numsSize; i++) {
          nums[i] = tmp[i];
        }
        exp *= base;
    }
}

基数排序 vs 计数排序 vs 桶排序

这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

十一、Tim排序

tim排序

Timsort是一个自适应的、混合的、稳定的排序算法,融合了归并算法和二分插入排序算法的精髓,在现实世界的数据中有着特别优秀的表现。

它是由Tim Peter于2002年发明的,用在Python这个编程语言里面。jdk1.6中使用排序用的是MergeSort, 1.7就换成了Timsort

这个算法之所以快,是因为它充分利用了现实世界的待排序数据里面,有很多子串是已经排好序的不需要再重新排序,利用这个特性并且加上合适的合并规则可以更加高效的排序剩下的待排序序列。

#define MIN(A, B) (A < B ? A : B)
const int RUN = 32;

void timSort(int *nums, int numsSize){
    for (int i = 0; i < numsSize; i += RUN){
        insertSort(nums + i, MIN((i + 32), numsSize - i));
    }

    for (int size = RUN; size < numsSize; size = 2 * size){
        for (int left = 0; left < numsSize; left += 2 * size){
            int mid = left + size - 1;
            int right = MIN((left + 2 * size - 1), (numsSize - 1));
            merge(nums, left, mid, right);
        }
    }
}
const RUN = 32;

function insertSort2(nums, left, right) {
    for (let i = left; i <= right; i++)
    {
        let tmp = nums[i];
        let j = i;
        for (; j > left && nums[j - 1] > tmp; j--) {
            nums[j] = nums[j - 1];
        }
        nums[j] = tmp;
    }
}
function timSort(nums, numsSize){

    for (let i = 0; i < numsSize; i += RUN){
        insertSort2(nums, i, Math.min((i + 32), numsSize - 1));
    }

    for (let size = RUN; size < numsSize; size = 2 * size){
        for (let left = 0; left < numsSize; left += 2 * size){
            let mid = left + size - 1;
            let right = Math.min((left + 2 * size - 1), (numsSize - 1));
            merge(nums, left, mid, right);
        }
    }
}

参考

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值