蓝桥杯《算法很美》第3章:排序与查找

181 阅读11分钟

3.1排序

1.冒泡排序

如图所示:

public class Test18 {
	public static void main(String[] args) {
		int[] arr = new int[] { 9, 1, 3, 66, 4, 15, 8, 7 };
		bubbleSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	/**
	 * 冒泡排序
	 * @param arr
	 */
	static void bubbleSort(int[] arr) {
		for (int i = arr.length - 1; i >= 0; i--) {
			for (int j = 0; j < i; j++) {
				if (arr[j] > arr[j + 1]) {
					int temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}
	}
}

2.选择排序

如图所示:

public class Test19 {
	public static void main(String[] args) {
		int[] arr = new int[] { 9, 1, 3, 66, 4, 15, 8, 7 };
		selectSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	/**
	 * 选择排序
	 * 
	 * @param arr
	 */
	static void selectSort(int[] arr) {
		for (int i = 0; i < arr.length - 1; i++) {
			for (int j = i + 1; j < arr.length; j++) {
				if (arr[i] > arr[j]) {
					int temp = arr[i];
					arr[i] = arr[j];
					arr[j] = temp;
				}
			}
		}
	}
}

3.插入排序

如图所示:

public class Test20 {
	public static void main(String[] args) {
		int[] arr = new int[] { 9, 1, 3, 66, 4, 15, 8, 7 };
		insertSort(arr);
		System.out.println(Arrays.toString(arr));
	}

	/**
	 * 选择排序
	 * 
	 * @param arr
	 */
	static void insertSort(int[] arr) {
		if (arr.length < 2)
			return;
		for (int i = 1; i < arr.length; i++) {
			for (int j = i; j > 0; j--) {
				if (arr[j] < arr[j - 1]) {
					int temp = arr[j];
					arr[j] = arr[j - 1];
					arr[j - 1] = temp;
				}
			}
		}
	}
}

4.快速排序

参考文章:

快速排序 = 冒泡 + 分治 + 递归

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public class Test21 {
	public static void main(String[] args) {
		int[] arr = new int[] { 3, 4, 7, 2, 4, 3, 1, 4, 5, 9 };
		quickSort(0, arr.length - 1, arr);
		System.out.println(Arrays.toString(arr));
	}

	/**
	 * 快速排序
	 * 
	 * @param left
	 * @param right
	 * @param arr
	 */
	static void quickSort(int left, int right, int[] arr) {
		if (left < right) {
			// 将小于arr[left]的值放左边,大于arr[left]的值放右边
			int index = partition(left, right, arr);
			// 对左边部分进行快速排序
			quickSort(left, index, arr);
			// 对右边部分进行快速排序
			quickSort(index + 1, right, arr);
		}
	}

	/**
	 * 将小于arr[left]的值放左边,大于arr[left]的值放右边,并返回新的left下标
	 * 
	 * @param left
	 * @param right
	 * @param arr
	 * @return
	 */
	private static int partition(int left, int right, int[] arr) {
		// eg: 【3, 4, 7, 2, 4, 3, 1, 4,5,9】
		// 以left位置的数为基准数
		int base = arr[left];
		int baseIndex = left;
		while (left < right) {
			// 找到比基准数base大的数的下标
			while (left < right && arr[right] >= base) {
				// 右指针前移
				right--;
			}
			// arr[left] = arr[right];

			// 找到比基准数base小的数的下标
			while (left < right && arr[left] <= base) {
				// 左指针后移
				left++;
			}
			// arr[right] = arr[left];
			// 交换左右指针位置的数
			int temp = arr[left];
			arr[left] = arr[right];
			arr[right] = temp;
		}
		// 基准数更新为当前左右指针重合位置的数
		arr[baseIndex] = arr[left];
		arr[left] = base;
		// 返回新的基准数下标
		return left;
	}
}

5.希尔排序

参考文章:希尔排序的原理,图解,java代码实现

6.归并排序

在这里插入图片描述

在这里插入图片描述

代码如下

public class Test22 {
	public static void main(String[] args) {
		int[] arr = new int[] { 8, 4, 5, 7, 1, 3, 6, 2 };
		int[] temp = new int[arr.length];
		mergeSort(arr, 0, arr.length-1, temp);
		
		System.out.println(Arrays.toString(arr));
	}

	/**
	 * 归并排序 = 拆分 + 处理 + 合并
	 * 
	 * @param arr 要排序的数组
	 * @param left 要排序的数组起始位置下标
	 * @param right 要排序的数组末尾下标
	 * @param temp 辅助临时数组
	 */
	static void mergeSort(int[] arr, int left, int right, int[] temp) {
		if (left < right) {
			// 中间索引下标
			int mid = left + ((right - left) >> 1);
			// 向左递归进行分解
			mergeSort(arr, left, mid, temp);
			// 向右递归进行分解
			mergeSort(arr, mid+1, right, temp);
			// 合并
			merge(arr, left, mid, right, temp);
		}
	}

	/**
	 * 合并: 治阶段
	 * 
	 * @param arr
	 * @param left
	 * @param mid
	 * @param right
	 * @param temp
	 */
	static void merge(int[] arr, int left, int mid, int right, int[] temp) {
		// 左边起始位置指针
		int i = left;
		// 右边起始位置指针
		int j = mid + 1;
		// 临时数组起始位置指针
		int t = 0;

		// 先把左右两边(有序)的数据按照规则填充到temp数组,直到左右两边的有序数列,有一边处理完毕为止
		while (i <= mid && j <= right) {
			// 如果左边的有序序列的当前元素,小于右边有序序列的当前元素
			if (arr[i] < arr[j]) {
				// 则将左边的当前元素,填充到temp数组
				temp[t] = arr[i];
				// 然后t++,i++
				t++;
				i++;
			} else {
				// 反之,则将右边有序序列的当前元素,填充到temp数组
				temp[t] = arr[j];
				// 然后t++,i++
				t++;
				j++;
			}
		}

		// 把有剩余数据的一边的数据,一次全部填充到temp
		// 左边未到头
		while (i <= mid) {
			temp[t] = arr[i];
			t++;
			i++;
		}
		// 右边未到头
		while (j <= right) {
			temp[t] = arr[j];
			t++;
			j++;
		}

		// 将temp数组拷贝回arr数组中
		// 临时数组初始位置
		t = 0;
		// 目标数组最左边的位置(最左边的起始位置不一定是0哦!)
		int l = left;
		while (l <= right) {
			arr[l] = temp[t];
			t++;
			l++;
		}
	}
}

练习题

1.超过一半的数字

数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。

解法思路

  • 解法1:排序后返回arr[N/2]

    • 一个数字在数组中出现次数超过了一半,则排序后,位于数组中间的数字一定就是该出现次数超过了长度一半的数字(可以用反证法证明),也即是说,这个数字就是统计学上的中位数。最容易想到的办法是用快速排序对数组排序号后,直接取出中间的那个数字,这样的时间复杂度为O(nlogn),空间复杂度为O(1)。

  • 解法2:hash统计

  • 解法3:顺序统计

    • 利用题2的按数值顺序的第K个元素值,O(N),限制:需要改动数组的内容
static int partition2(int a[],int p,int q){
    int privot=p;
    int left=p+1;
    int right=q;
    while(left<=right){
        while(left<=right&&a[left]<=a[privot])  left++;
        while(left<=right&&a[right]>a[privot])  right--;
        if(left<right){
            swap(a,right,left);
        }
    }
    swap(a,privot,right);
    return right;
}

public static int selectK(int[] A, int p, int r, int k) {
    int q = partition2(A, p, r);// 主元的下标
    int qK = q - p + 1;// 主元是第几个元素
    if (qK == k) return A[q];
    else if (qK > k) return selectK(A, p, q - 1, k);
    else return selectK(A, q + 1, r, k - qK);
  }

static void solve3(int[] arr) {
  int res = selectK(arr, 0, arr.length - 1, arr.length / 2);
  System.out.println(res);
}
  • 解法4:不同的数,进行消除

    • 由于该数字的出现次数比所有其他数字出现次数的和还要多,因此可以考虑在遍历数组时保存两个值:一个是数组中的一个数字,一个是次数,。当遍历到下一个数字时,如果下一个数字与之前保存的数字相同,则次数加1,如果不同,则次数减1,如果次数为0,则需要保存下一个数字,并把次数设定为1。由于我们要找的数字出现的次数比其他所有数字的出现次数之和还要大,则要找的数字肯定是组后一次把次数设为1时对应的数字。该方法的时间复杂度为O(n),空间复杂度为O(1)。
static int solve4(int[] arr) {
    int candidate=arr[0];//先定第一个元素候选
    int num=1;//出现次数
    for (int i = 1; i < arr.length; i++) {//从第二个扫描数组
        if(num==0){//前面的步骤相消,消为0了,把后面元素作为候选
            candidate=arr[i];
            num=1;
            continue;
        }
        if(candidate==arr[i]){//遇到和候选值相同的,次数加1
            num++;
        }else {//不同的数,进行消减
            num--;
        }
    }
    return candidate;
}

2.最小可用ID

在非负数组(乱序)中找到最小的可分配的id (从1开始编号),数据量1000000

代码如下:

//  O(N²) 暴力解法:从1开始依次探测每个自然数是否在数组中
static int find1(int[] arr) {
      int i = 1;
      while (true) {
        if (Util.indexOf(arr, i) == -1) {
          return i;
        }
        i++;
      }
}

//  NlogN
static int find2(int[] arr) {
      Arrays.sort(arr);//NlogN
      int i = 0;
      while (i < arr.length) {
        if (i + 1 != arr[i]) { //不在位的最小的自然数
          return i + 1;
        }
        i++;
      }
      return i + 1;
}

/**
 * 改进1:
 * 用辅助数组
 *
 */
public static int find3(int[] arr) {
      int n = arr.length;
      int[] helper = new int[n + 1];
      for (int i = 0; i < n; i++) {
        if (arr[i] < n + 1)
          helper[arr[i]] = 1;
      }
      for (int i = 1; i <= n; i++) {
        if (helper[i] == 0) {
          return i;
        }
      }
      return n + 1;
}

/**
 * 改进2,分区,递归
 * 问题可转化为:n个正数的数组A,如果存在小于n的数不在数组中,必然存在大于n的数在数组中, 否则数组排列恰好为1到n
 * @param arr
 * @param l
 * @param r
 * @return
 */
public static int find4(int[] arr, int l, int r) {
      if (l > r)
        return l + 1;
      int midIndex = l + ((r - l) >> 1);//中间下标
      int q = Case02_OrderStatistic.selectK(arr, l, r, midIndex - l + 1);//实际在中间位置的值
      int t = midIndex + 1;//期望值
      if (q == t) {//左侧紧密
        return find4(arr, midIndex + 1, r);
      } else {//左侧稀疏
        return find4(arr, l, midIndex - 1);
      }
}

3.合并两个有序数组

力扣题目链接:合并两个有序数组

0

4.逆序对个数

力扣题目链接:数组中的逆序对

一个数列,如果左边的数大,右边的数小,则称这两个数位一个逆序对。求出一个数列中有多少个逆序对。

解题思路

利用归并排序

在这里插入图片描述

在这里插入图片描述

如果选左边第一个,说明左边第一个比右边第一个小,所以左边第一个比右半部都小,类似,如果选右边第一个,说明右边第一个比左边第一个小,所以右边第一个比左半部都小,此时产生逆序对,个数为左边剩余全部的个数 ,即mid - left + 1

代码如下

class Solution {
    public int reversePairs(int[] nums) {
        if(nums.length<2){
            return 0;
        }
        int[] temp = new int[nums.length];
        mergeSort(nums, 0, nums.length-1, temp);
        return count;
    }

    /**
	 * 归并排序 = 拆分 + 处理 + 合并
	 * 
	 * @param arr   要排序的数组
	 * @param left  要排序的数组起始位置下标
	 * @param right 要排序的数组末尾下标
	 * @param temp  辅助临时数组
	 */
	void mergeSort(int[] arr, int left, int right, int[] temp) {
		if (left < right) {
			// 中间索引下标
			int mid = left + ((right - left) >> 1);
			// 向左递归进行分解
			mergeSort(arr, left, mid, temp);
			// 向右递归进行分解
			mergeSort(arr, mid + 1, right, temp);
			// 合并
			merge(arr, left, mid, right, temp);
		}
	}

	/**
	 * 逆序对个数
	 */
	int count = 0;

	/**
	 * 合并: 治阶段
	 * 
	 * @param arr
	 * @param left
	 * @param mid
	 * @param right
	 * @param temp
	 */
	void merge(int[] arr, int left, int mid, int right, int[] temp) {
		// 左边起始位置指针
		int i = left;
		// 右边起始位置指针
		int j = mid + 1;
		// 临时数组起始位置指针
		int t = 0;

		// 先把左右两边(有序)的数据按照规则填充到temp数组,直到左右两边的有序数列,有一边处理完毕为止
		while (i <= mid && j <= right) {
			// 如果左边的有序序列的当前元素,小于右边有序序列的当前元素
			if (arr[i] <= arr[j]) {
				// 则将左边的当前元素,填充到temp数组
				temp[t] = arr[i];
				// 然后t++,i++
				t++;
				i++;
			} else {
				// 反之,则将右边有序序列的当前元素,填充到temp数组
				temp[t] = arr[j];
				// 然后t++,i++
				t++;
				j++;
				// 逆序对个数累加
				count += mid - i + 1;
			}
		}

		// 把有剩余数据的一边的数据,一次全部填充到temp
		// 左边未到头
		while (i <= mid) {
			temp[t] = arr[i];
			t++;
			i++;
		}
		// 右边未到头
		while (j <= right) {
			temp[t] = arr[j];
			t++;
			j++;
		}

		// 将temp数组拷贝回arr数组中
		// 临时数组初始位置
		t = 0;
		// 目标数组最左边的位置(最左边的起始位置不一定是0哦!)
		int l = left;
		while (l <= right) {
			arr[l] = temp[t];
			t++;
			l++;
		}
	}
}

3.2树、二叉树介绍以及衍生的排序算法

  • 二叉树:

    • 根结点为0
    • 当前节点为i
    • 左儿子 2i+1
    • 右儿子 2i+2
    • 父节点(i-1)/2
  • 递归实现二叉树的三种遍历

    • 先序遍历:根 左 右
    • 中序遍历:左 根 右
    • 后序遍历:左 右 根
public class Test23 {
	public static void main(String[] args) {
		int a[] = new int[] { 78, 56, 34, 43, 4, 1, 15, 2, 23 };
		PreOrder(a, 0);
		System.out.println();
		InOrder(a, 0);
		System.out.println();
		PostOrder(a, 0);
	}

	/**
	 * 先序遍历
	 * 
	 * @param a
	 * @param i
	 */
	static void PreOrder(int[] arr, int i) {
		if (i >= arr.length)
			return;
		System.out.print(arr[i] + ",");
		PreOrder(arr, 2 * i + 1);
		PreOrder(arr, 2 * i + 2);
	}

	/**
	 * 中序遍历
	 * 
	 * @param a
	 * @param i
	 */
	static void InOrder(int[] arr, int i) {
		if (i >= arr.length)
			return;
		InOrder(arr, 2 * i + 1);
		System.out.print(arr[i] + ",");
		InOrder(arr, 2 * i + 2);
	}

	/**
	 * 后序遍历
	 * 
	 * @param a
	 * @param i
	 */
	static void PostOrder(int[] arr, int i) {
		if (i >= arr.length)
			return;
		PostOrder(arr, 2 * i + 1);
		PostOrder(arr, 2 * i + 2);
		System.out.print(arr[i] + ",");
	}
}

7.堆排序

堆的概念

  • 二叉堆是完全二叉树或者是近似完全二叉树。
  • 二叉堆满足二个特性:
    1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
    2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
  • 任意节点的值都大于其子节点的值—大顶堆
  • 任意节点的值都小于其子节点的值—小顶堆

堆排序

使记录序列按关键字非递减有序排列,则在堆排序的算法中先建一个“大顶堆”,即先选得-个关键字为最大的记录并与序列中最后一个记录交换,然后对序列中前n-1记录进行筛选,重新将它调整为一一个“大顶堆”,如此反复直至排序结束。由此,“筛选"应沿关键字较大的孩子结点向下进行。

public class Test {
    static void HeapAdjust(int[] H,int s,int m){
		//已知s.. m中记录的关键字除H[s]之外均满足堆的定义,本函数调整H[s]的关键字,使H[s..m]成为一个大顶堆
        int rc=H[s];//先把父亲节点值存到rc中
        for (int j = 2*s; j <=m ; j*=2) {//j为s的左孩子,循环一次后,j*2,变成最大孩子的左孩子
            if(j<m&&H[j]<H[j+1]){//如果右孩子大于左孩子,j++,j就为最大孩子的坐标
                j++;
            }
            if(rc>=H[j]){//父元素已经是两个孩子中最大的了,则for循环退出
                break;
            }
            H[s]=H[j];//把最大孩子的值赋值给父亲节点
            s=j;//s保存当前最大孩子的下标
        }
        H[s]=rc;//把父亲值赋值给最后一次孩子下标
    }
    //递归调整
    static void HeapAdjust2(int A[],int i,int n){//i为父节点,本函数使A[i..n]成为大顶堆
        int left=2*i;
        int right=2*i+1;
        if(left>n){//左孩子已经越界返回
            return;
        }
        int max=left;
        if(right<=n&&A[right]>A[left]){
            max=right;
        }//max指向左右孩子中较大的那个
        if(A[i]>=A[max]){//如果A[i]把;两个孩子都大,不同调整
            return;
        }
        _快速排序.swap(A,max,i); //否则,找到两个孩子中较大的,和i交换
        HeapAdjust2(A,max,n);  //大孩子那个位置的值发生了变化,i变更为大孩子那个位置,递归调整
    }
    static void HeapSort(int[] H){
        //H.length-1相当于H的length
        for(int i=(H.length-1)/2;i>0;i--){//把H.r[1.. H.length]建成大顶堆
            HeapAdjust2(H,i,H.length-1);//从(H.length-1)/2位最后一个父亲节点,依次往前调整
        }
        for (int i=H.length-1; i >1 ; i--) {
            _快速排序.swap(H,1,i);//将堆顶(1)记录和当前未经排序子序列H[1.. i]中最后一个记录相互交换
            HeapAdjust2(H,1,i-1);//将H[1..i-1]重新调整为大顶堆
        }
    }
    public static void main(String[] args) {
        //数组第一位预留,不参与排序
        int a[] = new int[]{0, 49, 38, 65, 97, 76, 13, 27, 49};
        HeapSort(a);
        System.out.println(Arrays.toString(a));
    }
}

8.桶排序

9.基数排序

参考韩顺平老师的视频:基数排序

基数排序是一种特殊的桶排序

  • 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或 bin sort,顾 名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
  • 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
  • 基数排序(Radix Sort)是桶排序的扩展
  • 基数排序是 1887 年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个 位数分别比较。

基数排序基本思想

  • 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
  • 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤:

将数组{53, 3, 542, 748, 14, 214}使用基数排序, 进行升序排序:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码如下

public class RadixSort {

	public static void main(String[] args) {
		int arr[] = { 53, 3, 542, 748, 14, 214};
		
		// 80000000 * 11 * 4 / 1024 / 1024 / 1024 =3.3G 
//		int[] arr = new int[8000000];
//		for (int i = 0; i < 8000000; i++) {
//			arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
//		}
		System.out.println("排序前");
		Date data1 = new Date();
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String date1Str = simpleDateFormat.format(data1);
		System.out.println("排序前的时间是=" + date1Str);
		
		radixSort(arr);
		
		Date data2 = new Date();
		String date2Str = simpleDateFormat.format(data2);
		System.out.println("排序前的时间是=" + date2Str);
		
		System.out.println("基数排序后 " + Arrays.toString(arr));
		
	}

	//基数排序方法
	public static void radixSort(int[] arr) {
		
		//根据前面的推导过程,我们可以得到最终的基数排序代码
		
		//1. 得到数组中最大的数的位数
		int max = arr[0]; //假设第一数就是最大数
		for(int i = 1; i < arr.length; i++) {
			if (arr[i] > max) {
				max = arr[i];
			}
		}
		//得到最大数是几位数
		int maxLength = (max + "").length();
		
		
		//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
		//说明
		//1. 二维数组包含10个一维数组
		//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
		//3. 名明确,基数排序是使用空间换时间的经典算法
		int[][] bucket = new int[10][arr.length];
		
		//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
		//可以这里理解
		//比如:bucketElementCounts[0] , 记录的就是  bucket[0] 桶的放入数据个数
		int[] bucketElementCounts = new int[10];
		
		
		//这里我们使用循环将代码处理
		
		for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
			//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
			for(int j = 0; j < arr.length; j++) {
				//取出每个元素的对应位的值
				int digitOfElement = arr[j] / n % 10;
				//放入到对应的桶中
				bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
				bucketElementCounts[digitOfElement]++;
			}
			//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
			int index = 0;
			//遍历每一桶,并将桶中是数据,放入到原数组
			for(int k = 0; k < bucketElementCounts.length; k++) {
				//如果桶中,有数据,我们才放入到原数组
				if(bucketElementCounts[k] != 0) {
					//循环该桶即第k个桶(即第k个一维数组), 放入
					for(int l = 0; l < bucketElementCounts[k]; l++) {
						//取出元素放入到arr
						arr[index++] = bucket[k][l];
					}
				}
				//第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
				bucketElementCounts[k] = 0;
				
			}
			//System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
			
		}
		
		/*
		
		//第1轮(针对每个元素的个位进行排序处理)
		for(int j = 0; j < arr.length; j++) {
			//取出每个元素的个位的值
			int digitOfElement = arr[j] / 1 % 10;
			//放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		int index = 0;
		//遍历每一桶,并将桶中是数据,放入到原数组
		for(int k = 0; k < bucketElementCounts.length; k++) {
			//如果桶中,有数据,我们才放入到原数组
			if(bucketElementCounts[k] != 0) {
				//循环该桶即第k个桶(即第k个一维数组), 放入
				for(int l = 0; l < bucketElementCounts[k]; l++) {
					//取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第l轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
			
		}
		System.out.println("第1轮,对个位的排序处理 arr =" + Arrays.toString(arr));
		
		
		//==========================================
		
		//第2轮(针对每个元素的十位进行排序处理)
		for (int j = 0; j < arr.length; j++) {
			// 取出每个元素的十位的值
			int digitOfElement = arr[j] / 10  % 10; //748 / 10 => 74 % 10 => 4
			// 放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		// 遍历每一桶,并将桶中是数据,放入到原数组
		for (int k = 0; k < bucketElementCounts.length; k++) {
			// 如果桶中,有数据,我们才放入到原数组
			if (bucketElementCounts[k] != 0) {
				// 循环该桶即第k个桶(即第k个一维数组), 放入
				for (int l = 0; l < bucketElementCounts[k]; l++) {
					// 取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第2轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
		}
		System.out.println("第2轮,对个位的排序处理 arr =" + Arrays.toString(arr));
		
		
		//第3轮(针对每个元素的百位进行排序处理)
		for (int j = 0; j < arr.length; j++) {
			// 取出每个元素的百位的值
			int digitOfElement = arr[j] / 100 % 10; // 748 / 100 => 7 % 10 = 7
			// 放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		// 遍历每一桶,并将桶中是数据,放入到原数组
		for (int k = 0; k < bucketElementCounts.length; k++) {
			// 如果桶中,有数据,我们才放入到原数组
			if (bucketElementCounts[k] != 0) {
				// 循环该桶即第k个桶(即第k个一维数组), 放入
				for (int l = 0; l < bucketElementCounts[k]; l++) {
					// 取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第3轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
		}
		System.out.println("第3轮,对个位的排序处理 arr =" + Arrays.toString(arr)); */
		
	}
}

10.计数排序

参考文章:计数排序和稳定的计数排序

10种常见排序算法对比

排序方法时间复杂度(平均)时间复杂度(最坏)时间复杂度(最好)空间复杂度稳定性
插入排序O(n^2)O(n^2)O(n)O(1)稳定
希尔排序O(n^1.3)O(n^2)O(n)O(1)不稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
堆排序O(nlog2n)O(nlog2n)O(nlog2n)O(1)不稳定
冒泡排序O(n^2)O(n^2)O(n)O(1)稳定
快速排序O(nlog2n)O(n^2)O(nlog2n)O(nlog2n)不稳定
归并排序O(nlog2n)O(nlog2n)O(nlog2n)O(n)稳定
桶排序O(n+k)O(n^2)O(n)O(n+k)稳定
基数排序O(n*k)O(n*k)O(n*k)O(n+k)稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)稳定

练习题

1.排序数组中找和的因子

  • 给定已排序数组arr和k ,不重复打印arr中所有相加和为k的不降序二元组
  • 如输入arr={-8,-4,-3,0,2,4,5,8,9,10},k=10
  • 输出(0,10)(2,8)
  • 扩展:三元组呢?

力扣例题:[剑指 Offer 57. 和为s的两个数字](leetcode-cn.com/problems/he…)

解题思路

在排序数组中借助双指针。

代码如下

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        int[] result = new int[2];

        while(left<=right){
            if((nums[left]+nums[right])<target){
               left++;
            }else if((nums[left]+nums[right])>target){
                right--;
            }else{
                result[0] = nums[left];
                result[1] = nums[right];
                return result;
            }
        }
        return result;
    }
}

2.需要排序的最短子数组

给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。你找到的子数组应是最短的,请输出它的长度。

示例 1:

输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

说明 :

输入的数组长度范围在 [1, 10,000]。
输入的数组可能包含重复元素 ,所以升序的意思是<=。

解题思路

在这里插入图片描述

代码如下

public int findUnsortedSubarray(int[] nums) {
   int[] a=nums.clone();
   Arrays.sort(a);
   int L=0,R=a.length-1;
   while (L<a.length&&a[L]==nums[L]) L++;
   while (R>0&&a[R]==nums[R]) R--;
   if(L==a.length&&R==0) return 0;
   return R-L+1;
}