有一些公司的笔试或者手撕代码会让写排序算法,因此进行相关的归纳,便于复习。
前言
- 排序动图参考了稀土掘金的
三分恶
博主的 万字长文|十大基本排序,一次搞定!文章。 - 代码或思路部分来源Java全栈体系的 排序算法 和 万字长文|十大基本排序,一次搞定!。
- 笔记笔记,有一些是自己才看得懂的tips,觉得不知所云的可以去搜官方的概念。
算法稳定性:如 BA1CDA2E,其中A1==A2,即相同值的关键字,在排序前后,原先在前面的A1位置保持在A2前面,那么此排序即为稳定。
冒泡排序
- 假设数组为
array = [A,B,C,D,E,F,G]
- 每次两两比对,移动下标,从
array[0]
和array[1]
、array[1]
和array[2]
,……,array[n-1]
和array[n]
- 【两两比对时,右边数值较小,则交换位置,否则不变,继续移动指针】
public void sort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
//升序
if (nums[i] > nums[j]) {
//交换
int temp = nums[i];
nums[j] = nums[i];
nums[i] = temp;
}
}
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
冒泡排序 | O(n²) | O(1) | 稳定 |
选择排序
- 每次遍历最小的元素
- 最初标记在最左边未排序的第一个元素(下标),往下遍历,最后的指针落在未排序最小的元素上
- 再将其与未排序第一个元素互换位置(一般从第一个数开始遍历查找,不会出现未排序元素<已排序元素)。
int min; // 无序区中最小元素位置
for(int i=0; i<n; i++) { // 有序区的末尾位置
min=i;
// 找出"a[i+1] ... a[n]"之间的最小元素,并赋值给min。
for(int j=i+1; j<n; j++) { // 无序区的起始位置
if(a[j] < a[min])
min=j;
}
// 若min!=i,则交换 a[i] 和 a[min]。
// 交换之后,保证了a[0] ... a[i] 之间的元素是有序的。
int tmp = a[i];
a[i] = a[min];
a[min] = tmp;
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
选择排序 | O(n²) | O(1) | 不稳定 |
注:基于链表的选择排序是稳定的,基于数组实现则不稳定。由于一般排序算法时默认用数组实现,因此默认其不稳定。
插入排序
- 取出无序区中的第1个数
X
,从有序区的右→左开始遍历,找出它在有序区对应的位置,即有序区中第一个小于X
的元素 - 将无序区的数据插入到有序区;一般需对有序区中的相关数据进行移位
int i, j, k;
for (i = 1; i < n; i++) { // 无序区
//为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置
for (j = i - 1; j >= 0; j--) // 有序区,若比有序区大则下一个
if (a[j] < a[i])
break;
//如找到了一个合适的位置
//将比a[i]大的数据向后移
int temp = a[i];
for (k = i - 1; k > j; k--)
a[k + 1] = a[k];
//将a[i]放到正确位置上
a[k + 1] = temp;
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
插入排序 | O(n²) | O(1) | 稳定 |
希尔排序
每次选取一个步长(即间隔),如第一次步长为4,则array[0]
和array[0+4]
两个元素为一组进行比较,再不断缩小步长(最后为1)比较
public void sort(int[] nums){
int step=nums.length; // 步长
while (step>0){
step=step/2; // 可自定义步长
//分组进行插入排序
for (int i=0;i<step;i++){
//分组内的元素,从第二个开始
for (int j=i+step;j<nums.length;j+=step){ // 分的小组内进行排序
int value=nums[j]; // 小组内的右边元素(j为右边元素下标)
int k;
for (k=j-step;k>=0;k-=step){ // 小组内的左边元素(K为左边元素下标)
if (nums[k]>value){
nums[k+step]=nums[k]; // 交换,右边元素赋值
}else {
break;
}
}
nums[k+step]=value; // 交换,左边元素赋值
}
}
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
希尔排序 | O(n^(1.3-2)) | O(1) | 不稳定 |
归并排序【递归】
- 拆分:将数组对半拆分组,直至单个(单个默认有序)一组
- 归并:两两组之间比较合并(从单个一组开始)
public static void merge(int[] a, int start, int mid, int end) {
int[] tmp = new int[end-start+1]; // tmp是汇总2个有序区的临时区域
int i = start; // 第1个有序区的索引
int j = mid + 1; // 第2个有序区的索引
int k = 0; // 临时区域的索引
while(i <= mid && j <= end) { // 归并汇总两个组至tmp[]数组
if (a[i] <= a[j])
tmp[k++] = a[i++];
else
tmp[k++] = a[j++];
}
// 汇总完,剩余某个组遗留(组内是有序的)元素,直接添加
while(i <= mid)
tmp[k++] = a[i++];
while(j <= end)
tmp[k++] = a[j++];
// 将排序后的元素,全部都整合到数组a中。
for (i = 0; i < k; i++){
a[start + i] = tmp[i];
}
tmp=null;
}
/*
* 归并排序(从上往下)
*/
public static void mergeSortUp2Down(int[] a, int start, int end) {
if(a==null || start >= end)
return ;
int mid = (end + start)/2;
mergeSortUp2Down(a, start, mid); // 递归排序a[start...mid]
mergeSortUp2Down(a, mid+1, end); // 递归排序a[mid+1...end]
// a[start...mid] 和 a[mid...end]是两个有序空间,
// 将它们排序成一个有序空间a[start...end]
merge(a, start, mid, end);
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
归并排序 | O(nlogn) | O(n) | 稳定 |
注:使用了一个临时数组来存储合并的元素,空间复杂度O(n)。
快速排序【分治】
- 选择一个元素
arr[i]
,值为X
(假设为起始位置) - 从右往左找到比
arr[i]
小的元素arr[j]
,将该元素赋值arr[i]
,可以想象为arr[j]
此时没有值,是个洞 - 由于
i
的数值变动了,此时从i+1
开始检索比X
大的数字,获得后,将其拿走替换到变动的位置arr[j]
,即把arr[j]
填洞(同时被拿走的位置相当于有了新的洞)
public static void quickSort(int[] a, int l, int r) {
if (l < r) {
int i,j,x;
i = l;
j = r;
x = a[i];
while (i < j) {
while(i < j && a[j] > x)
j--; // 从右向左找第一个小于x的数
if(i < j)
a[i++] = a[j]; // a[i]被赋值,同时i++
while(i < j && a[i] < x)
i++; // 从左向右找第一个大于x的数
if(i < j)
a[j--] = a[i];
}
a[i] = x;
quickSort(a, l, i-1); /* 递归调用 */
quickSort(a, i+1, r); /* 递归调用 */
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
快速排序 | O(nlogn) | O(1) | 不稳定 |
堆排序
堆:必须是完全二叉树;任一节点(即任意“父子”)的值必须是其子树的最大值或最小值
- 最大值时,称为“最大堆”,也称大顶堆;
- 最小值时,称为“最小堆”,也称小顶堆。
最大堆通常被用来进行"升序"排序,而最小堆通常被用来进行"降序"排序。
public void sort(int arr[]) {
int n = arr.length;
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐步将堆顶元素与末尾元素交换,然后重新构建大顶堆
for (int i = n - 1; i > 0; i--) {
// 将当前堆顶元素(最大值)与末尾元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 重新构建大顶堆
heapify(arr, i, 0);
}
}
// 构建大顶堆
void heapify(int arr[], int n, int i) {
int largest = i; // 最大值的索引
int left = 2 * i + 1; // 左子节点索引
int right = 2 * i + 2; // 右子节点索引
// 左子节点大于根节点
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 右子节点大于根节点
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是根节点,则交换并递归调整子树
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
heapify(arr, n, largest);
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
堆排序 | O(nlogn) | O(1) | 不稳定 |
计数排序
- 遍历获取数组最大值
max
,创建大小为max
的数组(有点浪费空间,可以用偏移量进行改进) - 将数组的每个值映射到下标(不适合有负数的数组,因为数组下标起始为0),在对应下标累计出现的次数
- 输出
public void sort(int[] nums) {
//查找最大值
int max = findMax(nums);
//寻找最小值
int min = findMin(nums);
//偏移量
int gap = max - min;
//创建统计次数新数组
int[] countNums = new int[gap + 1];
//将nums元素出现次数存入对应下标
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
countNums[num - min]++;
nums[i] = 0;
}
//排序
int index = 0;
for (int i = 0; i < countNums.length; i++) {
while (countNums[i] > 0) {
nums[index++] = min + i;
countNums[i]--;
}
}
}
public int findMax(int[] nums) {
int max = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
return max;
}
public int findMin(int[] nums) {
int min = nums[0];
for (int i = 0; i < nums.length; i++) {
if (nums[i] < min) {
min = nums[i];
}
}
return min;
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
计数排序 | O(n+k) | O(n) | 不稳定 |
注:虽然使用偏移量进行改进,但若是[33,1,9999,5]的数组,依旧需要建立 9999 - 1 = 9998
的空间,因此,计数排序不适合偏移量过大的数组。
桶排序
public void sort(int[] nums) {
int len = nums.length;
int max = nums[0];
int min = nums[0];
//获取最大值和最小值
for (int i = 1; i < len; i++) {
if (nums[i] > max) {
max = nums[i];
}
if (nums[i] < min) {
min = nums[i];
}
}
//计算步长
int gap = max - min;
//使用列表作为桶
List<List<Integer>> buckets = new ArrayList<>();
//初始化桶
for (int i = 0; i < gap; i++) {
buckets.add(new ArrayList<>());
}
//确定桶的存储区间
int section = gap / len - 1;
//数组入桶
for (int i = 0; i < len; i++) {
//判断元素应该入哪个桶
int index = nums[i] / section - 1;
if (index < 0) {
index = 0;
}
//对应的桶添加元素
buckets.get(index).add(nums[i]);
}
//对桶内的元素排序
for (int i = 0; i < buckets.size(); i++) {
//这个底层调用的是 Arrays.sort
// 这个api不同情况下可能使用三种排序:插入排序,快速排序,归并排序,具体看参考[5]
Collections.sort(buckets.get(i));
}
//将桶内的元素写入原数组
int index = 0;
for (List<Integer> bucket : buckets) {
for (Integer num : bucket) {
nums[index] = num;
index++;
}
}
}
假设输入的数组为 [9, 20, 35, 12, 6, 28, 15, 30, 3]。
1.获取最大值和最小值:
- max = 35, min = 3
2.计算步长:
- gap = max - min = 35 - 3 = 32
3.初始化桶:
- 创建一个大小为32的桶列表
4.确定桶的存储区间:
- section = gap / len - 1 = 32 / 9 - 1 = 2
5.数组入桶:
- 对于数组中的每个元素,根据
(元素 - min) / section,并向下取整数
,将元素添加到对应的桶中。
元素 9 入桶 3 、元素 20 入桶 9 、元素 35 入桶 16 、元素 12 入桶 4 、元素 6 入桶 1 、元素 28 入桶 9 、元素 15 入桶 6 、元素 30 入桶 12 、元素 3 入桶 06.对桶内的元素排序:
- 遍历桶列表,对每个桶内的元素进行排序。
桶 0 排序后:[3] 桶 1 排序后:[6] 桶 3 排序后:[9] 桶 4 排序后:[12] 桶 6 排序后:[15] 桶 9 排序后:[20, 28] 桶 12 排序后:[30] 桶 16 排序后:[35]7.将桶内的元素写入原数组:
- 遍历桶列表,将每个桶内的元素按照顺序写入原数组。
最终的数组排序结果为 [3, 6, 9, 12, 15, 20, 28, 30, 35]。
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
桶排序 | O(n+k) | O(n+k) | 取决于桶内排序所用算法的稳定性 |
基数排序
- 待排序数组
X
- 按照个位数进行排序,获得数组
X1
。 - 将
X1
按照十位数进行排序,获得数组X2
。 - 将
X2
按照百位数进行排序,便可获得一个有序序列 。
public void sort(int[] nums) {
int len = nums.length;
//最大值
int max = nums[0];
for (int i = 0; i < len; i++) {
if (nums[i] > max) {
max = nums[i];
}
}
//当前排序位置
int location = 1;
//用列表实现桶
List<List<Integer>> buckets = new ArrayList<>();
//初始化size为10的一个桶
for (int i = 0; i < 10; i++) {
buckets.add(new ArrayList<>());
}
while (true) {
//元素最高位数
int d = (int) Math.pow(10, (location - 1));
//判断是否排完
if (max < d) {
break;
}
//数据入桶
for (int i = 0; i < len; i++) {
//计算余数 放入相应的桶
int number = ((nums[i] / d) % 10);
buckets.get(number).add(nums[i]);
}
//写回数组
int nn = 0;
for (int i = 0; i < 10; i++) {
int size = buckets.get(i).size();
for (int ii = 0; ii < size; ii++) {
nums[nn++] = buckets.get(i).get(ii);
}
buckets.get(i).clear();
}
location++;
}
}
排序 | 平均时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|
基数排序 | O(n+k) | O(n+k) | 稳定 |