排序算法总结

概述

请添加图片描述

基本概念

Θ 渐进紧确界

存在正常量 n 0 , c 1 , c 2,使得
c 1 g ( n ) ≤ f ( n ) ≤ c 2 g ( n ) w h e r e : n ≥ n 0 c_1g(n)\leq f(n)\leq c_2g(n) where :n\ge n_0 c1g(n)f(n)c2g(n)where:nn0
那么称为g ( n ) g(n)g(n)为f ( n ) f(n)f(n)的渐进紧确界,记为:
f ( n ) = Θ ( g ( n ) ) f(n)= \Theta(g(n)) f(n)=Θ(g(n))

同理可理解 O 渐进上界与Ω 渐进下界

递归式时间复杂度

一般情况下,算法的时间递归式都可以写成如下形式:
T ( n ) = a T ( n / b ) + f ( n ) T(n)=aT(n/b)+f(n) T(n)=aT(n/b)+f(n)
表示为:规模为 n,算法所耗费的时间等于划分时间 f(n) 加上同样算法作用在规模为n/b 的时间T(n/b)的 a 倍。

将递归式一直写下去:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1u4EtJoY-1659503636941)(面试准备-排序算法/image-20220802165342258.png)]

可以看出 n == b^m时,问题规模变为1.此时公式变为:
T ( n ) = a log ⁡ b n T ( 1 ) + ∑ j = 0 log ⁡ b n − 1 a j f ( n / b j ) T(n) = a^{\log_b n} T(1) + \sum_{j =0}^{\log_b n-1} a^j f(n/b^j) T(n)=alogbnT(1)+j=0logbn1ajf(n/bj)
因为

log ⁡ b n log ⁡ b a = log ⁡ b a log ⁡ b n \log_b^{n\log_b^a}=\log_b^a\log_b^n logbnlogba=logbalogbn

log ⁡ b a log ⁡ b n = log ⁡ b n log ⁡ b a \log_b^{a\log_b^n}=\log_b^n\log_b^a logbalogbn=logbnlogba

所以
a log ⁡ b n = n log ⁡ b a a\log_b^n =n\log_b^a alogbn=nlogba
化简为
T ( n ) = Θ ( n log ⁡ b a ) + Θ [ ∑ j = 0 log ⁡ b n − 1 a j f ( n / b j ) ] T(n) = \Theta(n^{\log_b^a}) + \Theta[\sum_{j =0}^{\log_b n-1} a^j f(n/b^j)] T(n)=Θ(nlogba)+Θ[j=0logbn1ajf(n/bj)]

快排举例

递归式
T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n) = 2T(n/2) + \Theta (n) T(n)=2T(n/2)+Θ(n)
代入之前化简式子为:
T ( n ) = Θ ( n ) + Θ ( ∑ j = 0 log ⁡ 2 n − 1 n ) = Θ ( n ) + Θ ( n log ⁡ 2 n ) = Θ ( n log ⁡ 2 n ) = Ω ( n l o g n ) T(n) = \Theta(n) + \Theta(\sum_{j =0}^{\log_2 n-1}n)=\Theta(n)+\Theta(n\log_2^n)=\Theta(n\log_2^n) = \Omega(nlogn) T(n)=Θ(n)+Θ(j=0log2n1n)=Θ(n)+Θ(nlog2n)=Θ(nlog2n)=Ω(nlogn)
在n特别大的时候可以忽略底数,因此一般就写作log

请添加图片描述

堆排序

  1. 如果为升序排列,将数组
    转换为堆之后,从最后一个父节点开始,将最大的数作为父节点,遍历整个堆。
  2. 将根节点与最后一个结点的位置交换
  3. 重新形成大顶堆,将排好序的元素排除出堆
  4. 重复2~3,直到堆只剩下一个元素
    请添加图片描述
//交换位置
void Swap(int* a,int* b){
	int temp = *a;
    *a = *b;
    *b = temp;
}

void MaxHeapify(int arr[], int start,int end){
    int dad = start;
    int son = dad * 2 - 1;
    while(son<=end){
        if(son + 1 <=end && arr[son] < arr[son + 1])		son++;
        if(arr[dad] > arr[son])	return;
        //如果父节点最大就返回,后续重复步骤重新排序时能节省时间
        else{
            Swap(&arr[dad],&arr[son]);
            //继续将其与孙结点比较
            dad = son;
            son = dad * 2 - 1;
        }
    }
}

void HeapSort(int arr[],int length){
	for(int i = length / 2 - 1; i >= 0; i--){
        MaxHeapify(arr,i,length - 1);
    }
    for(int i = length - 1; i > 0; i--){
        Swap(&arr[0],&arr[i]);
        MaxHeapify(arr,0,i - 1);
        //最后一个元素有序,将堆的长度减少1
    }
}

归并排序

归并排序算法将数组分为两半,对每部分递归地应用归并排序。在两部分都排好序后,对它们进行归并。

请添加图片描述

int min(int x, int y) {
    return x < y ? x : y;
}
void merge_sort(int arr[], int len) {
    int *a = arr;
    int *b = (int *) malloc(len * sizeof(int));
    int seg, start;
    //从最开始一段只有一个,慢慢merge
    for (seg = 1; seg < len; seg += seg) {
        for (start = 0; start < len; start += seg * 2) {
            //增加每一段的数量,到达下一段
            int low = start, mid = min(start + seg, len), high = min(start + seg * 2, len);
            //获取接下来的两段
            int k = low;
            int start1 = low, end1 = mid;
            int start2 = mid, end2 = high;
            //开始归并
            while (start1 < end1 && start2 < end2)
                b[k++] = a[start1] < a[start2] ? a[start1++] : a[start2++];
            while (start1 < end1)
                b[k++] = a[start1++];
            while (start2 < end2)
                b[k++] = a[start2++];
        }
        //确保a数组有序,这样才能正确归并
        int *temp = a;
        a = b;
        b = temp;
    }
    //如果a指向的不是原本数组,那么就是b指向的原有数组。
    //但有序的是a,因此要复制到b数组
    if (a != arr) {
        int i;
        for (i = 0; i < len; i++)
            b[i] = a[i];
        b = a;
    }
    free(b);
}

递归版:

void merge_sort_recursive(int arr[], int reg[], int start, int end) {
    if (start >= end)	return;
    //只有一个元素时返回
    int len = end - start, mid = (len >> 1) + start;
    int start1 = start, end1 = mid;
    int start2 = mid + 1, end2 = end;
    merge_sort_recursive(arr, reg, start1, end1);
    merge_sort_recursive(arr, reg, start2, end2);
    int k = start;
    while (start1 <= end1 && start2 <= end2)
        reg[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    while (start1 <= end1)
        reg[k++] = arr[start1++];
    while (start2 <= end2)
        reg[k++] = arr[start2++];
    for (k = start; k <= end; k++)
        arr[k] = reg[k];
}

void merge_sort(int arr[], const int len) {
    int reg[len];
    merge_sort_recursive(arr, reg, 0, len - 1);
}

快速排序

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort_recursive(int arr[], int start, int end) {
    if (start >= end)
        return;
    int mid = arr[start];
    //一般为了防止最坏情况出现,一般随机选取
    int left = start, right = end;
    while (left < right) {
        while (arr[right] >= mid && left < right)
            right--;
        while (arr[left] <= mid && left < right)
            left++;
        if(left < right)
       		swap(&arr[left], &arr[right]);
    }
	//和基准值交换,让基准值始终在中间,使其不用再次排序
	swap(&arr[left], &arr[start]);
	quick_sort_recursive(arr, start, left - 1);
    quick_sort_recursive(arr, left + 1, end);
}

void quick_sort(int arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

希尔排序

希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本

希尔排序的基本思想是:希尔排序把序列看作是一个矩阵,分成 𝑚 列,逐列进行排序,从某个整数逐渐减为1 ;当 𝑚 为1时,整个序列将完全有序

实例步骤:

请添加图片描述
请添加图片描述

void shell_sort(int arr[], int len) {
        int gap, i, j;
        int temp;
        for (gap = len >> 1; gap > 0; gap >>= 1)
                for (i = gap; i < len; i++) {
                        temp = arr[i];
                        for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap)
							arr[j + gap] = arr[j];
                        arr[j + gap] = temp;
                }
}

计数排序

计数排序要求输入的数据必须是有确定范围的整数。当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

核心思想:8 个数比 A 小,那 A 就排在第 9 位

//用来计数的数组长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1)
//本例为100
void counting_sort(int *ini_arr, int *sorted_arr, int n) {
        int *count_arr = (int *) malloc(sizeof(int) * 100);
        int i, j, k;
    	//初始化计数数组
        for (k = 0; k < 100; k++)
                count_arr[k] = 0;
        for (i = 0; i < n; i++)
                count_arr[ini_arr[i]]++;
    	//count_arr数组由于保存每个元素的第几号小,第一号小的数组下标就为0
        for (k = 1; k < 100; k++)
                count_arr[k] += count_arr[k - 1];
    	//有重复时需要特殊处理,这就是为什么最后要反向填充目标数组
    	//--的目的是为了下一个相同大小元素排序时,排在前面(保证稳定性)
        for (j = n; j > 0; j--)
                sorted_arr[--count_arr[ini_arr[j - 1]]] = ini_arr[j - 1];
        free(count_arr);
}

先找到最大值max和最小值min,然后呢,从索引0开始依次存放3~8出现的次数,每个次数累加上其前面的所有次数,得到的就是元素在有序序列中的位置信息。

请添加图片描述

  • 如果元素不重复,array中的元素 k 对应的 counts 索引是 k – min
  • 如果重复,array中的元素 k 对应的 counts 索引是 k – min-p,p 代表着是倒数第几个 k

桶排序

桶排序是计数排序的升级版。基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序

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

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

请添加图片描述

public static void bucketsort(int[] arr) {
		ArrayList bucket[] = new ArrayList[5];// 声明五个桶
		for (int i = 0; i < bucket.length; i++) {
			bucket[i] = new ArrayList<Integer>();// 确定桶的格式为ArrayList
		}
		for (int i = 0; i < arr.length; i++) {
            // 确定元素存放的桶号
			int index = arr[i] / 10;
            // 将元素存入对应的桶中
			bucket[index].add(arr[i]);
		}
		for (int i = 0; i < bucket.length; i++) {
            // 遍历每一个桶
			// 对每一个桶排序,但元素分配不均匀时,算法退化到桶内使用的算法
            bucket[i].sort(null);
		}
}

基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,依次对个位数、十位数、百位数、千位数、万位数…进行排序(从低位到高位)

请添加图片描述

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

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;
void radixsort(int *a, int n) {
    //MAX代表最大为多少
	int i, b[MAX], m = a[0], exp = 1;

  	for (i = 1; i < n; i++) {
    	if (a[i] > m) {
     		m = a[i];
    	}
  	}

  	while (m / exp > 0) {
    	int bucket[10] = { 0 };

    	for (i = 0; i < n; i++) {
      		bucket[(a[i] / exp) % 10]++;
    	}
        
		//和桶排序思想类似,记录序号
    	for (i = 1; i < 10; i++) {
      		bucket[i] += bucket[i - 1];
    	}

    	for (i = n - 1; i >= 0; i--) {
      		b[--bucket[(a[i] / exp) % 10]] = a[i];
   		}

    	for (i = 0; i < n; i++) {
      		a[i] = b[i];
    	}
    	exp *= 10;
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值