一些知识
稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。
非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。
原地排序:指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换。
非原地排序:需要利用额外的数组来辅助排序。
使用计数、基数排序算法对负数排序时,可以先预处理让其变为正数,排序完毕后再复原,比如统一加减;
常见稳定排序有:冒泡排序、插入排序、归并排序
常见非稳定排序有:选择排序、希尔排序、堆排序、快速排序
十大排序 1.冒泡排序 冒泡排序就是比较相邻的两个元素,把小的元素往前调(把大的元素往后调),每次循环将最大的元素移动到后面;
步骤:对每一对相邻元素,如果第一个比第二个大,就交换他们两个;
时间O(n^2)、空间O(1)、稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void bubbleSort (vector<int >& nums) { int len = nums.size (); for (int i = 0 ; i < len - 1 ; ++i) { bool flag = false ; for (int j = 0 ; j < len - 1 - i; ++j) { if (nums[j] > nums[j + 1 ]) { swap (nums[j], nums[j + 1 ]); flag = true ; } } if (!flag) break ; } }
2.选择排序 选择排序是给每个位置选择剩余未排序元素中最小的元素,然后交换;
步骤:
在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
以此类推,直到所有元素均排序完毕;
时间O(n^2)、空间O(1)、不稳定
1 2 3 4 5 6 7 8 9 10 void selectSort (vector<int >& nums) { int len = nums.size (); for (int i = 0 ; i < len; ++i) { int minIndex = i; for (int j = i + 1 ; j < len; ++j) { if (nums[j] < nums[minIndex]) minIndex = j; } swap (nums[i], nums[minIndex]); } }
3.插入排序 插入排序是在一个已经有序的小序列的基础上,每次寻找合适的插入位置来插入一个元素。刚开始这个有序的小序列就是第一个元素,每次从有序序列的末尾倒序开始寻找插入位置。
步骤:
从第一个元素开始,该元素可以认为已经被排序
取出下一个元素,在已经排序的元素序列中从后向前扫描
如果该元素(已排序)大于新元素,将该元素移到前一位置
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
将新元素插入到该位置后
重复步骤2~5
时间O(n^2)、空间O(1)、稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void insertionSort (vector<int >& a) { int n = a.size (); for (int i = 1 ; i < n; ++i) { if (a[i] < a[i - 1 ]) { int j = i - 1 ; int x = a[i]; while (j >= 0 && x < a[j]) { a[j + 1 ] = a[j]; --j; } a[j + 1 ] = x; } } }
4.快速排序
每次处理完后基准位置是有序的,然后递归处理左右区间;
步骤:
选取第一个数为基准
将比基准小的数交换到前面,比基准大的数交换到后面
对左右区间递归 重复第二步,直到各区间只有一个数
时间O(nlogn)、空间O(logn)、不稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void quickSort (vector<int >& a, int left, int right) { if (left >= right) return ; int first = left; int last = right; int key = a[first]; while (first < last) { while (first < last && a[last] > key) --last; if (first < last) a[first++] = a[last]; while (first < last && a[first] <= key) ++first; if (first < last) a[last--] = a[first]; } a[first] = key; quickSort (a, left, first - 1 ); quickSort (a, first + 1 , right); }
5.希尔排序
希尔排序是插入排序的一种变种。无论是插入排序还是冒泡排序,如果原数组的一个元素距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素 以对数组的局部进行排序。
希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。间隔也称作增量。
时间O(nlogn)、空间O(1)、不稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void shellSortCore (vector<int >& nums, int gap, int i) { int inserted = nums[i]; int j = i - gap; while (j >= 0 && inserted < nums[j]) { nums[j + gap] = nums[j]; j -= gap; } nums[j + gap] = inserted; }void shellSort (vector<int >& nums) { int len = nums.size (); for (int gap = len / 2 ; gap > 0 ; gap /= 2 ) { for (int i = gap; i < len; ++i) { shellSortCore (nums, gap, i); } } }
6.归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。适用于处理较大规模的数据,但比较耗内存。
归并排序有自顶向下的递归方法,和自底向上的迭代方法;
即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并 。
步骤:
把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序;
将两个排序好的子序列合并成一个最终的排序序列;
时间O(nlogn)、空间O(n)、稳定
①递归,辅助数组传引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void mergeSortCore (vector<int >& nums, vector<int >& temp, int left, int right) { if (left >= right) return ; int mid = left + (right - left) / 2 ; int start1 = left, end1 = mid; int start2 = mid + 1 , end2 = right; mergeSortCore (nums, temp, start1, end1); mergeSortCore (nums, temp, start2, end2); int index = left; while (start1 <= end1 && start2 <= end2) { temp[index++] = nums[start1] < nums[start2] ? nums[start1++] : nums[start2++]; } while (start1 <= end1) temp[index++] = nums[start1++]; while (start2 <= end2) temp[index++] = nums[start2++]; for (int i = left; i <= right; ++i) nums[i] = temp[i]; }void mergeSort (vector<int >& nums) { vector<int > temp (nums.size(), 0 ) ; mergeSortCore (nums, temp, 0 , nums.size () - 1 ); }
②递归,开销更大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void mergeTwoVec (vector<int >& nums, int left, int mid, int right) { int start1 = left, end1 = mid; int start2 = mid + 1 , end2 = right; vector<int > temp (right - left + 1 , 0 ) ; int index = 0 ; while (start1 <= end1 && start2 <= end2) { temp[index++] = nums[start1] < nums[start2] ? nums[start1++] : nums[start2++]; } while (start1 <= end1) temp[index++] = nums[start1++]; while (start2 <= end2) temp[index++] = nums[start2++]; index = 0 ; for (int i = left; i <= right; ++i) nums[i] = temp[index++]; }void mergeSort (vector<int >& nums, int left, int right) { if (left >= right) return ; int mid = left + (right - left) / 2 ; mergeSort (nums, left, mid); mergeSort (nums, mid + 1 , right); mergeTwoVec (nums, left, mid, right); }
7.堆排序 基于堆(大根堆、小根堆)的一种排序算法,每次从堆顶取出一个元素;
堆是采用vertor<TimeNode>
数组实现的,数组下标从0开始,对应堆顶节点;
对于数组中下标为k
的某节点,其左子节点为2*k + 1
,右子节点为2*k + 2
,父节点为(k - 1) / 2
;
插入节点时,插入到数组中的最后一个节点的后面,然后与该节点的父节点比较大小,如果插入的元素小于父节点元素,那么与父节点交换位置。重复上述步骤直到大于父节点元素或者到达堆顶。该过程叫做上浮 ,即插入时上浮;
移除节点时,将该节点与末尾节点交换,然后当前节点(之前的尾部节点)与子节点中的较小者比较,如果当前节点大于较小子节点,那么与较小子节点交换位置,重复上述步骤直到小于较小子节点或者到达倒数第二个节点为止。最后再删除末尾节点。该过程叫做下沉 ,即移除元素时下沉;
排序时无需插入节点上浮构建堆,而是将待排数组看着完全二叉树,不断下沉节点,从而变成大顶堆;
步骤:
时间O(nlogn)、空间O(1)、不稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void heapSink (vector<int >& nums, int heapSize, int i) { int maxIdx = i; int left = i * 2 + 1 ; int right = i * 2 + 2 ; if (left < heapSize && nums[left] > nums[maxIdx]) maxIdx = left; if (right < heapSize && nums[right] > nums[maxIdx]) maxIdx = right; if (maxIdx != i) { swap (nums[maxIdx], nums[i]); heapSink (nums, heapSize, maxIdx); } }void heapBuild (vector<int >& nums) { int lastRoot = ((nums.size () - 1 ) - 1 ) / 2 ; for (int i = lastRoot; i >= 0 ; --i) { heapSink (nums, nums.size (), i); } }void heapSort (vector<int >& nums) { heapBuild (nums); int heapSize = nums.size (); while (heapSize > 1 ) { swap (nums[0 ], nums[heapSize - 1 ]); --heapSize; heapSink (nums, heapSize, 0 ); } }
8.计数排序 统计小于等于某元素值的元素的个数count,然后将该元素就放在目标数组的索引i位(i = count, i ≥ 0
);
计数排序基于一个假设,待排序数列的所有数均为整数,且出现在[min, max]的大小区间中;max - min 过大则会引起较大的空间复杂度,一般是用来排序 0 到 100 之间的数字的最好的算法,但是它不适合按字母顺序排序人名。速度快于任何比较排序算法 。
步骤:
找出待排序的数组中最大和最小的元素;
统计数组中每个值为 i 的元素出现的次数,存入数组 Cnt 的第 i 项;
对所有的计数累加(从 Cnt 中的第一个元素开始,每一项和前一项相加);
填充目标数组:将每个元素 i 放在新数组的第 Cnt[i] 项,每放一个元素就将 Cnt[i] 减去 1;
时间O(n+k)、空间O(k)、稳定 (k = maxNum - minNum + 1)
①利用差值减小空间复杂度;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void countSort (vector<int >& nums) { int n = nums.size (); if (n <= 1 ) return ; int minNum = INT_MAX, maxNum = INT_MIN; for (int num : nums) { if (num < minNum) minNum = num; if (num > maxNum) maxNum = num; } int len = maxNum - minNum + 1 ; vector<int > vecCount (len, 0 ) ; for (int num : nums) ++vecCount[num - minNum]; int index = 0 ; for (int i = 0 ; i < len; ++i) { while (vecCount[i] != 0 ) { nums[index++] = i + minNum; --vecCount[i]; } } }
②普通计数排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void countSort (vector<int >& nums) { int n = nums.size (); if (n == 0 ) return ; vector<int > temp (n, 0 ) ; auto it = max_element (nums.begin (), nums.end ()); int len = *it + 1 ; vector<int > vecCount (len, 0 ) ; for (int num : nums) ++vecCount[num]; for (int i = 1 ; i < len; ++i) vecCount[i] += vecCount[i - 1 ]; for (int i = n - 1 ; i >= 0 ; --i) { int num = nums[i]; int cnt = vecCount[num]; int idx = cnt - 1 ; temp[idx] = num; --vecCount[num]; } swap (nums, temp); }
9.桶排序 将值为i的元素放入i号桶,最后依次把桶里的元素倒出来。大编号桶里的所有元素均大于小编号桶里的任意元素;
步骤:
设置一个定量的数组当作桶;
寻访序列,并且把项目一个一个放到对应的桶中;
对每个不是空的桶子进行排序 (使用其它排序算法);
从非空桶里把取回排序好的元素;
时间O(n+k)、空间O(n+k)、稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 void bucketSort (vector<int >& nums) { int n = nums.size (); if (n <= 1 ) return ; int minNum = INT_MAX, maxNum = INT_MIN; for (int num : nums) { if (num < minNum) minNum = num; if (num > maxNum) maxNum = num; } int bucketNum = 5 ; int bucketSize = (maxNum - minNum) / bucketNum + 1 ; vector<vector<int >> buckets (bucketNum, vector <int >(0 )); for (int num : nums) { int bucketIndex = (num - minNum) / bucketSize; buckets[bucketIndex].emplace_back (num); } for (auto & bucket : buckets) { sort (bucket.begin (), bucket.end ()); } int index = 0 ; for (auto & bucket : buckets) { for (int num : bucket) { nums[index++] = num; } } }
10.基数排序 一种多关键字的排序算法,对数组中所有数依次按由低到高的位数 进行多次排序;
步骤:
取得数组中的最大数,并取得位数;
arr为原始数组,从最低位开始取每个位组成radix数组;
对radix进行计数排序(利用计数排序适用于小范围数的特点);
时间O(n x k)、空间O(n x k)、稳定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int getBitNum (vector<int >& nums) { int maxValue = INT_MIN; for (int num : nums) { if (num > maxValue) maxValue = num; } int bitCount = 1 ; while (maxValue >= 10 ) { maxValue /= 10 ; ++bitCount; } return bitCount; }void radixSortCore (vector<int >& nums, int divisor) { vector<int > countBit (10 , 0 ) ; for (int num : nums) { int x = (num / divisor) % 10 ; ++countBit[x]; } for (int i = 1 ; i <= 9 ; ++i) countBit[i] += countBit[i - 1 ]; vector<int > temp (nums.size(), 0 ) ; for (int i = nums.size () - 1 ; i >= 0 ; --i) { int x = (nums[i] / divisor) % 10 ; int cnt = countBit[x]; temp[cnt - 1 ] = nums[i]; --countBit[x]; } swap (nums, temp); }void radixSort (vector<int >& nums) { int n = nums.size (); if (n <= 1 ) return ; int maxBitLen = getBitNum (nums); int divisor = 1 ; for (int i = 0 ; i < maxBitLen; ++i) { radixSortCore (nums, divisor); divisor *= 10 ; } }
【参考资料】力扣题解、阿秀笔记