一、Java TreeSet
TreeSet是通过TreeMap实现的一个有序的、不可重复的集合,底层维护的是红黑树结构。
当TreeSet的泛型对象不是java的基本类型的包装类时,对象需要重写Comparable#compareTo()
方法
具体参考知乎
二、数组旋转
最近在写LeetCode的时候,遇到了很多数组旋转的问题,因此决定在这里做个总结,可能不全,后续再补充。
【问题】:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
2.1 多次反转
先反转全部数组,在反转前k个,最后在反转剩余的,如下:
public void rotate(int[] nums, int k) {
int length = nums.length;
k %= length;
reverse(nums, 0, length - 1);//先反转全部的元素
reverse(nums, 0, k - 1);//在反转前k个元素
reverse(nums, k, length - 1);//接着反转剩余的
}
//把数组中从[start,end]之间的元素两两交换,也就是反转
public void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start++] = nums[end];
nums[end--] = temp;
}
}
当然也可以再调整下,先反转前面的,接着反转后面的k个,最后在反转全部,原理都一样
2.2 环形旋转
类似约瑟夫环一样,把数组看作是环形的,每一个都往后移动k位
但这里有一个坑,如果nums.length%k=0
,也就是数组长度为k的倍数,这个会原地打转,对于这个问题我们可以使用一个数组visited表示这个元素有没有被访问过,如果被访问过就从他的下一个开始,防止原地打转。
public static void rotate(int[] nums, int k) {
int hold = nums[0];
int index = 0;
int length = nums.length;
boolean[] visited = new boolean[length];
for (int i = 0; i < length; i++) {
index = (index + k) % length;
if (visited[index]) {
//如果访问过,再次访问的话,会出现原地打转的现象,
//不能再访问当前元素了,我们直接从他的下一个元素开始
index = (index + 1) % length;
hold = nums[index];
i--;
} else {
//把当前值保存在下一个位置,保存之前要把下一个位置的
//值给记录下来
visited[index] = true;
int temp = nums[index];
nums[index] = hold;
hold = temp;
}
}
}
三、堆排序扩展
【补充】:优先级对队列结构,就是堆结构(对于Java,PriorityQueue底层就是堆结构)
扩展题目
已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k
,并且k
相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
【解题思路】:假设k = 6
,准备一个小根堆,遍历前7个数字放入小根堆,此时小根堆的最小值一定是整个数组的最小值(因为任何一个数的位置与它排完序的位置距离都不会超过k = 6
),将小根堆的最小值弹出并放入数组的0位置处,把下一个数(第8个数)放入小根堆,重复…重复,直到将整个数组排好序。
复杂度分析:每个数放入小根堆的操作为
O
(
l
o
g
k
)
O(log\ k)
O(log k),所以时间复杂度为
O
(
N
l
o
g
k
)
O(Nlog\ k)
O(Nlog k),当k很小的时候,就可认为这是一个
O
(
N
)
O(N)
O(N)的算法。
【代码实现】:
public class SortArrayDistanceLessK {
/**
* 已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,
* 并且k相对于数组来说比较小。请选择一个合适的排序算法针对这个数据进行排序。
*/
public static void sortArrayDistanceLessK(int[] arr, int k) {
// 默认小根堆
Queue<Integer> heap = new PriorityQueue<>();
int index = 0;
for (; index <= k; index++) {
heap.add(arr[index]);
}
int i = 0;
for (;!heap.isEmpty() && index < arr.length; index++, i++) {
arr[i] = heap.poll();
heap.add(arr[index]);
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
}
小根堆在Java中就是优先级队列,因此可以直接使用PriorityQueue
,但是需要注意几点:
- 扩容问题:
PriorityQueue
在底层是用数组作为堆的实际结构,但它不像我们手写的堆,提前规定了整个数组的大小,PriorityQueue
在add()
时若发现空间不够会进行成倍扩容,单次扩容代价是 O ( N ) O(N) O(N)(数组拷贝)- 若一共
add()
了 N N N个数,则会经历 l o g N log\ N log N 次扩容。 - 所以扩容的总代价为 O ( N ∗ l o g N ) O(N*log\ N) O(N∗log N),每个数平均扩容代价为 O ( l o g N ) O(log N) O(logN)
- 若一共
- ⭐️无法改变值:
PriorityQueue
无法处理这样的情况:改变某一个节点的值,并对该节点执行heapify/heapInsert
操作,PriorityQueue
只能删除该节点后,添加一个新数。- 这也是为什么很多面试场合不得不手写堆的原因,我们自己写的堆是可以单独对某一个节点进行
heapify/heapInsert
的,需要重点分辨
- 这也是为什么很多面试场合不得不手写堆的原因,我们自己写的堆是可以单独对某一个节点进行
四、桶排序思想下的排序
1、计数排序
算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
适用场景
排序目标要能够映射到整数域,其最大值最小值应当容易辨别。
高中生考试的总分数,显然用0-750就OK啦(不考虑小数);又比如一群人的年龄,用个0-150应该就可以了,再不济就用0-200。另外,计数排序需要占用大量空间,它比较适用于数据比较集中的情况。
当排序范围/数量非常大的时候,就要慎重考虑这种排序方法
2、基数排序
排序过程:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成一个桶;
- 对arr中的每个数首先根据个位从左到右依次放入各个桶中,然后按照从左到右、先进先出的顺序将所有的数从桶中拿出来
- 再根据十位、百位、…、最高位,重复操作3。
图源知乎
虽然算法思路是这样的但是代码可以写的更灵活一点(再次感叹左神老师是真的🐂)
public class RadixSort {
/**
* 基数排序算法描述
* 1. 取得数组中的最大数,并取得位数;
* 2. arr为原始数组,从最低位开始取每个位组成一个桶;
* 3. 对arr中的每个数首先根据个位从左到右依次放入各个桶中,然后按照从左到右、先进先出的顺序将所有的数从桶中拿出来
* 4. 再根据十位、百位、…、最高位,重复操作3。
*/
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
/**
* 求数组中最大数的位数
*
* @param arr 数组
* @return 最大数的位数
*/
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i : arr) {
max = Math.max(i, max);
}
int count = 0;
while (max != 0) {
count++;
max /= 10;
}
return count;
}
/**
* 对数组的指定范围进行基数排序————通用的写法
*
* @param arr 数组
* @param l 左边界
* @param r 右边界
* @param digit 要排序的数中,最大数的位数
*/
public static void radixSort(int[] arr, int l, int r, int digit) {
final int radix = 10;
int i = 0, j = 0;
// 有多少个数就准备多少个辅助空间
int[] bucket = new int[r - l + 1];
// 有多少位就进出多少次
for (int d = 1; d < digit; d++) {
// 10个空间
// count[0], 当前位i是0的数有多少个
// count[1], 当前位i是1的数有多少个
// ...
int[] count = new int[radix];
// 计算处在d位上各个数字对应的arr中数据量
for (i = l; i < radix; i++) {
j = getDigit(arr[i], d);
count[j]++;
}
// 求count的前缀和
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
for (i = r; i >= l; i--) {
j = getDigit(arr[i], d);
bucket[(count[j]--) - 1] = arr[i];
}
// 拷贝回arr
for (i = l, j = 0; i <= r; i++, j++) {
arr[i] = bucket[j];
}
}
}
/**
* 返回num的第d位数字
*/
public static int getDigit(int num, int d) {
return ((num / ((int) Math.pow(10, d - 1))) % 10);
}
}
实战:最大间距
- leetcode 原题:164. 最大间距 - 力扣(LeetCode)
- 难度等级: Hard