十大排序算法

以长度为n=20的数组nums=[1,3,5,7,2,6,4,8,9,2,8,7,6,0,3,5,9,4,1,0]为例。要求:升序。

相关概念:

  • 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

img

1. 冒泡排序

算法描述

两层遍历。

  • 外层遍历:每一次循环确认未排序元素中最大元素的最终位置。共遍历n-1个元素。
  • 内层遍历:每一次循环,比较相邻元素的大小。若前者比后者大,则交换,否则不交换。遍历次数为未排序元素的个数-1。
代码实现
void bubble_sort(vector<int> &nums, int n) {
    for(int i = 0; i < n - 1; ++i) {
        for(int j = 0; j < n - i - 1; ++j) {
            if(nums[j] > nums[j+1]) {
                int temp = nums[j];
                nums[j] = nums[j+1];
                nums[j+1] = temp;
            }
        }
    }
}
复杂度分析
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

2. 选择排序

算法描述

两层遍历。

  • 外层遍历:每一次循环确认未排序元素中最小元素的最终位置。共遍历n-1个元素。
  • 内层遍历:每一次循环,寻找未排序元素中的最小值,将它与未排序的第一个元素交换位置。遍历次数为未排序元素的个数-1。
代码实现
void selection_sort(vector<int> &nums, int n) {
    int k;
    for(int i = 0; i < n - 1; ++i) {
        k = i;
        for(int j = i + 1; j < n; ++j) {
            if(nums[j] < nums[k])
      			k = j;
        }
        if(k != i) {
            int temp = nums[i];
            nums[i] = nums[k];
            nums[k] = temp;
        }
    }
}
复杂度分析
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

3. 插入排序

算法描述

基本思想:无序序列插入到有序序列中,数组的第一个元素默认有序。

两层遍历。

  • 外层遍历:每一次循环,将未排序的第一个元素插入到有序序列的正确位置。共遍历n-1个元素。
  • 内层遍历:每一次循环,待插入元素依次从有序序列的最后一个元素开始进行比较。如果待插入元素更小,被比较元素向后挪一个位置。如果待插入元素更大,则直接插入到被比较元素的后一个位置。遍历次数看情况。
代码实现
void insertion_sort(vector<int> &nums, int n) {
    for(int i = 1; i < n; ++i) {
        int cur = nums[i];
        int j;
        for(j = i-1; j >= 0 && cur < nums[j]; --j) {
            nums[j+1] = nums[j];
        }
        nums[j+1] = cur;
    }   
}
复杂度分析
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定

4. 希尔排序

算法描述

实质上是分组插入排序。事先定义一个增量数组,用来分组。

三层遍历:

  • 外层遍历:遍历增量数组。一般采取[n/2, n/2/2, …, 1]
  • 中间遍历:遍历各个分组。
  • 内层遍历:组内实现插入排序。
代码实现
void shell_sort(vector<int> &nums, int n) {
    for(int gap = n / 2; gap > 0; gap /= 2) {
    	for(int i = gap; i < n; ++i) {
            int cur = nums[i];
            int j;
            for(j = i - gap; j >= 0 && cur < nums[j]; j -= gap) {
                nums[j+gap] = nums[j];
            }
            nums[j+gap] = cur;
        }
    }
}
复杂度分析
  • 时间复杂度:O(n^(1.3-2))
  • 空间复杂度:O(1)
  • 稳定性:不稳定

5. 归并排序

算法描述

分治思想。

递归算法。

  1. 把长度为n的数组分成两个长度n/2的子数组;
  2. 对两个子数组分别采用归并排序;
  3. 直到两个子数组长度分别为1(默认看作是有序数组);
  4. 将两个子数组合并成一个有序数组,直到最后合并成一个完整的有序数组。(两个有序数组合并成一个大的有序数组,之间题目里做到过,利用双指针)
代码实现
void merge_sort(vector<int> &nums, int l, int r, vector<int> &temp) {
    // 边界条件
    if(l + 1 >= r)  return;
    
    // divide
    int m = l + (r - l) / 2;
    merge_sort(nums, l, m, temp);
    merge_sort(nums, m, r, temp);
    
    // conquer
    int i = l, j = m, k = l;
    while(i < m || j < r) {
        if(j >= r || (i < m && nums[i] <= nums[j])) {
            // 后一个区间排完了,把前一个区间全部排一下
            // 或者,两者都没排完的情况下,满足前一个区间当前元素更小
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }
    for(i = l; i < r; ++i) {
        nums[i] = temp[i];
    }
}
复杂度分析
  • 时间复杂度:O(nlog_2n)
  • 空间复杂度:O(n)
  • 稳定性:稳定

6. 快速排序

算法描述

分治思想。

递归算法。

  1. 从数组中选出一个元素作为基准(pivot);
  2. 把数组中所有小于基准的放在基准前面(前半区),大于基准的放在基准后面(后半区);
  3. 重复上述操作,递归遍历两个半区,直到有序。
代码实现
void quick_sort(vector<int> &nums, int l, int r) {
    if (l + 1 >= r) {
        return;
    }
    int first = l, last = r - 1, key = nums[first];
    while (first < last){
        while(first < last && nums[last] >= key) {
        	--last;
    	}
        nums[first] = nums[last];
        while (first < last && nums[first] <= key) {
            ++first;
        }
    	nums[last] = nums[first];
    }
    nums[first] = key;
    quick_sort(nums, l, first);
    quick_sort(nums, first + 1, r);
}
复杂度分析
  • 时间复杂度:O(nlog2n)
  • 空间复杂度:O(nlog2n)
  • 稳定性:不稳定

堆排序

算法描述

基本思想:保证每一个堆的根节点大于(或小于)其左右叶子结点。

一般升序采用大顶堆,降序采用小顶堆。

  1. 构造初始堆

    对于一个无序序列,从最后一个非叶子结点(nums.size()/2 - 1)开始,从左至右,从下至上进行调整,保证每个堆符合条件,最后就能得到一个大顶堆。

  2. 堆排序

    两次遍历。

    外层遍历:确认待排序数组中的最大元素。将堆顶元素和待排序末尾元素进行交换,使得待排序中的末尾元素是最大的。

    内层遍历:每一次交换,都需要调整堆。

代码实现
void HeapAdjust(vector<int> &nums, int start, int end) {
    int head = start;	// 当前结点的下标
    int l = head * 2 + 1;		// 当前结点的左孩子
    int r = head * 2 + 2;		// 当前结点的右孩子
    if(l < end && nums[l] > nums[head]) {
        head = l;
    }
    if(r < end && nums[r] > nums[head]) {
        head = r;
    }
    if(head != i) {
        int temp = nums[i];
        nums[i] = nums[head];
        nums[head] = temp;
        // 递归
        HeadAdjust(nums, head, end);
    }
}

void heap_sort(vector<int> &nums, int n) {
    // 构建大顶堆
    for(int i = n / 2 - 1; i >=0; --i) {
        HeapAdjust(nums, i, n);
    }
    // 堆排序
    for(int i = n - 1; i > 0; --i) {
        int temp = nums[i];
        nums[i] = nums[0];
        nums[0] = temp;
        HeapAdjust(nums, 0, i)
    }
}
复杂度分析
  • 时间复杂度:O(nlog2n)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

计数排序

算法描述

利用数组下标确定元素的正确位置

三次遍历。

  • 第一次遍历,找到数组的最大元素和最小元素;
  • 第二次遍历,统计数组各个元素个数;
  • 第三次遍历,根据统计数组,得到正确的有序数组。
代码实现
void count_sort(vector<int> &nums, int n) {
    // 找到数组的最大最小元素
    int min = nums[0], max = nums[0];
    for(int i = 1; i < n; ++i) {
        if(nums[i] < min)	min = nums[i];
        if(nums[i] > max)	max = nums[i];
    }
    // 统计数组
    int l = max - min + 1;
    int *count = new int[l]();
    for(int i = 0; i < n; ++i) {
        ++count[nums[i]-min];
    }
    // 数组填充
    int cur = 0;
    for(int i = 0; i < max; ++i) {
        for(int j = count[i]; j > 0; --j) {
            nums[cur++] = min + i;
        }
    }
}
复杂度分析
  • 时间复杂度:
  • 空间复杂度:
  • 稳定性:

桶排序

算法描述

可以看作是计数排序的扩展版本,计数排序的count数组,每个下标桶存储相同元素,而桶排序每个下标桶存储一定范围的元素。

  1. 根据映射函数(自定),把数组里的元素划分到各个桶;
  2. 各个桶自排;
  3. 依次取出桶中元素,完成最后的正确排序。
代码实现
void bucket_sort(vector<int> &num, int n) {
    // 找到数组的最大最小元素
    int min = nums[0], max = nums[0];
    for(int i = 1; i < n; ++i) {
        if(nums[i] < min)	min = nums[i];
        if(nums[i] > max)	max = nums[i];
    }
    // 统计数组
    // 此处运用的映射规则:每个下标存储相同元素(同计数排序)
    int l = max - min + 1;
    int *count = new int[l]();
    for(int i = 0; i < n; ++i) {
        ++count[nums[i]-min];
    }
    // 数组填充
    int cur = 0;
    for(int i = 0; i < max; ++i) {
        for(int j = count[i]; j > 0; --j) {
            nums[cur++] = min + i;
        }
    }
}
复杂度分析
  • 时间复杂度:O(n+k) k表示桶的个数
  • 空间复杂度:O(n+k)
  • 稳定性:稳定

基数排序

算法描述

排序 + 收集

先按低位排序,然后收集;再按高位排序,然后收集;以此类推直到最高位。

代码实现
void radix_sort(vector<int> &nums, int n) {
    // 先得到数组中的最大值的位数
    int max = nums[0];
    for(int i = 1; i < n; ++i) {
        if(max < nums[i])	
            max = nums[i];
    }
    int count = 0;
    while(max > 0) {
        max /= 10;
        ++count;
    }
    // 排序 + 收集
    vector<vector <int>> temp(10);
    for(int i = 0, mod = 10, dev = 1; i < count; ++i, dev *= 10, mod *= 10) {
        for(int j = 0; j < n; ++j) {
            int loca = nums[j] % mod / dev;   // 获得相应位数上的数字
            temp[loca].push_back(nums[j]);
        }
        int cur = 0;
        for(int j = 0; j < 10; ++j) {
            for(int k = 0; k < temp[j].size(); ++k) {
                nums[cur++] = temp[j][k];
            }
        }
        // 清空temp
        temp.clear();
    }
}
复杂度分析
  • 时间复杂度:O(n*k)
  • 空间复杂度:O(n+k)
  • 稳定性:稳定

参考:

  1. 十大经典排序算法(动图演示)
  2. 常见的7种排序算法
  3. 十大经典排序算法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值