说明:下述内容除了少许图片选取自他人网站,其余均为本人独创。
0、排序算法导论
排序的概念
数据结构中的一个重点概念就是内部排序,内部排序是指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。其功能是对一个数据元素集合或序列重新排列成一个按数据元素某个相知有序(递增,递减)的序列。
排序的分类
- 插入类排序:直接插入排序、希尔排序。
- 交换类排序:冒泡排序、快速排序。
- 选择排序:简单选择排序、堆排序。
- 归并排序。
- 基数排序。
- 桶排序。
- 计数排序。
术语说明
- 稳定的排序: 排序过程中有前后两个元素相同的点,待排序结束后,这两个相同的点的相对前后位置不发生变化。
- 不稳定的排序: 排序过程中有前后两个元素相同的点,待排序结束后,这两个相同的点的相对前后位置发生了变化。
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:执行完一个程序所需内存的大小。
算法种类、时间复杂度、空间复杂度、是否稳定
out-place:占用额外内存。
k:桶的个数。
算法分类
注意:本博客涉及到的所有排序算法均是升序排序!
1、直接插入排序
算法描述
直接插入排序的过程是最开始固定头一个元素,然后在第二个元素开始,从前面已经排好序的序列中,选择一个合适的位置,将待插入元素插入到前面排好序的序列中。方法是从后向前的扫描序列,每扫描一次元素向后移动一个位置。
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
int i = 1, j = 0;
for(; i < nums.size(); i++){
int tmp = nums[i];
for(j = i; j > 0; j--){
if(tmp < nums[j - 1])
nums[j] = nums[j - 1];
else
break;
}
nums[j] = tmp; // 该赋值语句要写在for循环的外面,否则当待排序元素需要排在第0个位置时,无法正确处理。
}
return nums;
}
};
运行截图
2、希尔排序
算法描述
希尔排序也称为缩小增量排序,是直接插入类排序的改进方法,时间复杂度在。算法思想是每次选一定的增量为一个分组,在该分组中进行直接插入排序。下一轮减小增量,再使用相同的方法, 直到增量为1为止,对全体元素执行整体的直接插入排序算法。
严蔚敏的数据结构课本中的希尔排序法的增量为:5, 3, 1,这也是一般的计算机考研中数据结构题目中的希尔排序增量。
下面的代码中的增量是数组长的,随后依次2倍递减。
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
int len = nums.size(), tmp = 0;
int gap = len / 2;
for(; gap; gap /= 2){
for(int i = gap; i < len; i++){
tmp = nums[i];
int preIndex = i - gap;
while(preIndex >= 0 and nums[preIndex] > tmp){
nums[preIndex + gap] = nums[preIndex];
preIndex -= gap;
}
nums[preIndex + gap] = tmp;
}
}
return nums;
}
};
运行截图
3、冒泡排序
算法描述
冒泡排序的过程是从左至右依次比较两个相邻的元素,若前面的元素大于后面的元素,则交换两个元素,否则不执行操作。待指针从左至右遍历结束后,数组中的最大值便被交换到了最右边,依稀类推,直到在某一轮遍历的过程中没有元素发生交换为止。
由于整个过程就像冒泡一样,所以名为冒泡排序。
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
int i = nums.size() - 1, j = 0;
for(; i > 0; i--){
bool flag = true; // 设置flag是冒泡排序的优化算法
for(j = 0; j < i; j++){
if(nums[j] > nums[j + 1]){
swap(nums[j], nums[j + 1]);
flag = false;
}
}
if(flag)
break;
}
return nums;
}
};
运行截图
4、快速排序
算法描述
快速排序也是交换类排序的一种。思想是通过设置一个点为枢轴(一般为数组中的第一个元素),通过一趟排序之后,枢轴位于序列的中部位置,要求枢轴的左边元素均小于枢轴,枢轴的右边元素均大于枢轴。再分别递归对枢轴左边的元素和枢轴右边的元素执行快速排序直到数列整体有序为止。
具体实现:设置指针指向序列的最右边元素,依次从右向左遍历数组,找到第一个不大于枢轴的元素,将枢轴的值与该值交换,然后再从左向右依次遍历数组找到第一个不小于枢轴的值,将枢轴的值与该值交换,指针再指向右边没有遍历的位置开始遍历,方法与上述相同,以此类推,直到枢轴处于数组的中部无可交换的元素为止。第一轮快速排序结束,再分别对枢轴两边的序列递归调用快速排序的方法,直到整体元素有序为止。
动态图和下述代码的枢轴选取略有不符。
代码实现(严蔚敏《数据结构》定义枢轴函数写法)
class Solution {
public:
int partition(vector& nums,int low,int high){
int pivot = nums[low];
while(low < high){
while(low < high and nums[high] >= pivot)
--high;
nums[low] = nums[high];
while(low < high and nums[low] <= pivot)
++low;
nums[high] = nums[low];
}
nums[low] = pivot;
return low;
}
void QuickSort(vector& nums,int low,int high){
if(low < high){
int pivotpos = partition(nums, low, high);
QuickSort(nums, low, pivotpos - 1);
QuickSort(nums, pivotpos + 1, high);
}
}
vector sortArray(vector& nums) { // 主函数
int n = nums.size();
QuickSort(nums, 0, n - 1);
return nums;
}
};
运行截图
显然时间复杂度要快于上面的希尔排序。
代码实现(考研《天勤高分笔记》的写法)
class Solution {
public:
void quickSort(vector& nums, int left, int right) {
if (left >= right)
return;
int cur = left + 1; // 从左侧第二个元素开始
int low = left; // 分界点为第一个元素
while (cur <= right) {
if (nums[cur] <= nums[left]) { // 交换位置保证low的左侧都是小于num[left]
swap(nums[low + 1], nums[cur]);
low++;
}
cur++;
}
swap(nums[left], nums[low]); // 把分界点元素移动到新的分界位置
quickSort(nums, left, low - 1);
quickSort(nums, low + 1, right);
}
vector sortArray(vector& nums) { //主函数
int n = nums.size();
quickSort(nums, 0, n - 1);
return nums;
}
};
运行截图
5、简单选择排序
算法描述
简单选择排序是选择类排序,算法思想是从左至右遍历数组,首先固定数组中的第一个元素,分别与剩余的所有元素进行比较,从而找到序列中的最小值和固定元素交换,如果固定元素就是最小值,则无需交换。以此类推,直到整体元素有序为止。
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
int n = nums.size();
for(int i = 0; i < n; i++){
int m = i;
for(int j = i + 1; j < n; j++){
if(nums[m] > nums[j])
m = j;
}
swap(nums[i], nums[m]);
}
return nums;
}
};
运行截图
6、堆排序
算法描述
堆排序属于选择排序,是简单选择排序的优化。算法思想:排序为升序,建立大顶堆。
- 将数组中的元素从前向后依次抽象成一个完全二叉树,并保证根结点的值大于孩子结点的值,它的每棵子树保持这个性质。
- 这就需要对二叉树的值进行调整,最下面最右面的子树开始,由下到上调整,如果孩子结点的值大于父结点的值,则将孩子结点的值和父结点的值进行交换,以此类推,直到满足上面的性质为止。
- 此时最大值存在于根结点,将根结点与最右边叶子结点的值进行交换,此时,整个序列的最大值被存放在整个数组的最右边。
- 再将剩余的个序列按照从根结点与其左右孩子比较的方法调整为大顶堆,再与倒数第二个结点进行交换。
- 循环上述过程,直到所有结点全部有序。
可见下面的两幅图,方法原理都是一样的。
算法实现
class Solution {
public:
vector sortArray(vector& nums) {
int len = nums.size();
buildMaxHeap(nums, len); // 构建一个大顶堆 升序排列
return nums;
}
void buildMaxHeap(vector& nums, int len){
//第一个for循环 从下到上 调整为大顶堆
for(int i = len / 2 - 1; i >= 0; i--)
adjustHeap(nums, i, len); // 逐一调整为大顶堆
for(int i = len - 1; i >= 0; i--){
swap(nums[0], nums[i]); // 交换根结点和最后的结点 最大值放在最后
adjustHeap(nums, 0 , i); // 对剩下的序列从上到下调整为大顶堆
}
}
// 从上到下 调整为大顶堆
void adjustHeap(vector& nums, int node, int len){
int left = 2 * node + 1;
int right = 2 * node + 2;
int max = node; // 定义max 存储某棵子树的最大结点下标
if(left < len and nums[left] > nums[max])
max = left; // 存在左孩子 左孩子结点大于其父结点
if(right < len and nums[right] > nums[max])
max = right; // 判断该子树 哪一个结点大于父结点
if(max != node){ // 如果真存在子结点大于父结点的情况 进行交换
swap(nums[max], nums[node]);
adjustHeap(nums, max, len);// 交换后 判断子树结点是否满足大顶堆的性质
}
}
};
运行截图
7、归并排序
算法描述
该算法的是采用分治法(Divide and Conquer)。将已有序的子序列合并,得到完全有序的序列;
- 先使每个子序列有序,再使子序列段间有序。在严蔚敏《数据结构与算法》课本中,将两个有序表合并成一个有序表,称为2-路归并排序。
- 把长度为的序列分成两个长度为的子序列;
- 对这两个子序列使用递归分别再采用归并排序;
-
将两个排序好的子序列合并成一个最终的排序序列。
2-路归并排序动态图:
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
int n = nums.size();
MergeSort(nums, 0, n - 1);
return nums;
}
void MergeSort (vector& nums, int low,int high) {
if(low >= high)
return; // 终止递归的条件,子序列长度为1
int mid = low + (high - low) / 2; // 取得序列中间的元素
MergeSort(nums, low, mid); // 对左半边递归
MergeSort(nums, mid + 1, high); // 对右半边递归
Merge(nums, low, mid, high); // 合并
}
void Merge(vector& nums, int low, int mid, int high){
//low为第1有序区的第1个元素,i指向第1个元素, mid为第1有序区的最后1个元素
int i = low,j = mid + 1, k = 0; //mid+1为第2有序区第1个元素,j指向第1个元素
int *temp = new int[high - low + 1]; //temp数组暂存合并的有序序列
while(i <= mid && j <= high){
if(nums[i] <= nums[j]) //较小的先存入temp中
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
while(i <= mid)//若比较完之后,第一个有序区仍有剩余,则直接复制到t数组中
temp[k++] = nums[i++];
while(j <= high)//同上
temp[k++] = nums[j++];
for(i=low, k=0; i <= high; i++,k++)//将排好序的存回arr中low到high这区间
nums[i] = temp[k];
delete [] temp;//释放内存,由于指向的是数组,必须用delete []
}
};
运行截图
8、基数排序
算法描述
基数排序是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn)
为数组长度,k
为数组中的数的最大的位数;
- 基数排序是按照低位先排序,然后收集;
- 再按照高位排序,然后再收集;
- 以此类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
代码实现(java实现,只允许有非负数)
class Solution {
public int[] sortArray(int[] nums) {
if (nums.length < 2)
return array;
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
max = Math.max(max, nums[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList> bucketList = new ArrayList>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < nums.length; j++) {
int num = (nums[j] % mod) / div;
bucketList.get(num).add(nums[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
nums[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return nums;
}
}
9、计数排序
算法描述
计数排序的过程是创建一个长度为数组中最小和最大元素之差的数组,分别对应数组中的每个元素,然后用这个新的数组来统计每个元素出现的频率,然后遍历新的数组,根据每个元素出现的频率把元素放回到老的数组中,得到已经排好序的数组。
代码实现
class Solution {
public:
vector sortArray(vector& nums) {
if (nums.size() == 0)
return nums;
int min = *min_element(nums.begin(), nums.end());
int max =*max_element(nums.begin(), nums.end());
int bias = 0 - min;
vector bucket(max - min + 1, 0);
for (int i = 0; i < nums.size(); i++) {
bucket[nums[i] + bias]++;
}
int index = 0, i = 0;
while (index < nums.size()) {
if (bucket[i] != 0) {
nums[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return nums;
}
};
运行截图
该方法通过牺牲空间复杂度的方法换取时间复杂度的减少,是目前在LeetCode平台上这些算法中运行最快的方法。
当然,该方法也可以通过哈希表来实现。
代码实现(map哈希表方法)
class Solution {
public:
vector sortArray(vector& nums) {
map m;
for(int& i : nums)
m[i]++;
int n = nums.size(), i = 0;
while(i < n){
if(m.begin()->second){
nums[i++] = m.begin()->first;
m.begin()->second--;
}
else
m.erase(m.begin());
}
return nums;
}
};
运行截图
上述代码的思路就是计数排序,但使用哈希表,时间复杂度大为增加,这值得引人思考。
10、桶排序
算法描述
首先新建一个桶的数组,每个桶的规则需要提前制定好,比如元素在为一个桶、为一个桶。然后遍历整个待排序的数组,把元素分配到对应的桶里面。接下来单独对每个桶里面的元素进行排序,排序算法可以选择比较排序或者非比较排序,得到排序后的数组。最后把所有的桶内的元素还原到原数组里面得到最后的排序数组。
代码实现
class Solution {
public int[] sortArray(int[] nums) {
int INTERVAL = 100; // 定义桶的大小
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int num : nums) { // 找到数组元素的范围
min = Math.min(min, num);
max = Math.max(max, num);
}
int count = (max - min + 1); // 计算出桶的数量
int bucketSize = (count % INTERVAL == 0) ?( count / INTERVAL) : (count / INTERVAL+1);
List[] buckets = new List[bucketSize];
for (int num : nums) { // 把所有元素放入对应的桶里面
int quotient = (num-min) / INTERVAL;
if (buckets[quotient] == null) buckets[quotient] = new ArrayList<>();
buckets[quotient].add(num);
}
int cur = 0;
for (List bucket : buckets) {
if (bucket != null) {
bucket.sort(null); // 对每个桶进行排序
for (Integer integer : bucket) { // 还原桶里面的元素到原数组
nums[cur++] = integer;
}
}
}
return nums;
}
}
运行截图
总结
通过上述数据结构十大算法的讲解和LeetCode系统的运行,时间复杂度在级别的显示超时,只有时间复杂度稍低的才会显示通过。在上述过程中运行最快的是计数排序,它是通过牺牲空间复杂度来换取的高效运行,其次是快速排序,快速排序的空间复杂度的消耗也相对比较大,但小于计数排序。
综合比较,每个排序算法各有其特点,不同的排序算法适应与不同的场景。