排序算法
基础排序,时间复杂度O(n2)
- 直接插入排序(稳定)
- 冒泡排序(稳定)
- 选择排序(不稳定)
进阶排序,时间复杂度O(nlogn)
- 快排(不稳定)
- 归并(稳定)
- 堆排(不稳定)
1. 直接插入排序(稳定):从i=1开始遍历,提取nums[i]作为标准,排序[insertIndex,i-1]区间,排序完成之后,将nums[i]插入到insertIndex+1位置
- 时间复杂度:O(n2),最好On
- 空间复杂度:O(1)
- 稳定:在排序后的序列后面插入,就算是相等,相对位置没有发生变化,稳定
- 基本实现
private static void insertSort(int[] nums) {
//1. 判断边界条件
if(nums.length < 2){
return;
}
//2. 外层从左往右,内层从i往左
for(int i=1;i=0 && nums[insertIndex] > insertNum;insertIndex--){
nums[insertIndex + 1] = nums[insertIndex];
}
//排序好之后,将insertNum插入到insertIndex+1
nums[insertIndex+1] = insertNum;
}
}
2. 冒泡排序(稳定):j循环[0,nums.length-1-i],前后比较,如果前比后大,交换
- 时间复杂度:On2,最好On
- 空间复杂度:O(1)
- 稳定:只是交换相邻两个元素,如果存在两个相等的相邻元素,不交换,相对位置没有发生变化。因此是稳定的
- 具体实现
public void bubbleSort(int[] nums){
if(nums.length < 2){
return;
}
for(int i=0;i nums[j+1]){
swap(nums,j,j+1);
}
}
}
}
- 优化实现(创建判断标志位flag,内层循环如果发生交换,flag = false。外层循环完成一次后,进行判断flag,如果flag为true,结束排序)
public void bubbleSort(int[] nums){
if(nums.length < 2){
return;
}
for(int i=0;i nums[j+1]){
swap(nums,j,j+1);
flag = false;
}
}
if(flag){
break;
}
}
}
3. 选择排序(不稳定):提取当前索引作为min,遍历从[i+1,nums.length]找到比nums[min]小的值,提取它的索引。遍历完成后,如果min不等于i,交换i和min的值
- 时间复杂度;O(n2)
- 空间复杂度:O(1)
- 不稳定:因为要提取基准值作为判断标准,如果比基准值小,基准值和j要进行交换,相对位置可能会发生变化。如[5,8,5,2,10]
private static void selectSort(int[] nums) {
//1. 判断
if(nums.length < 2){
return;
}
for(int i=0;i
4. 归并排序(稳定):将数组分成两半([0,middle]和[middle+1,len])
- 时间复杂度:
- 平均O(nlogn)
- 最好O(nlogn)
- 最坏O(nlogn)
- 空间复杂度:O(n)
- 稳定:相等元素在合并时并不会改变相对位置。稳定
- 具体实现:
private static void MergeSort(int[] nums,int start,int end) {
//1. 判断越界
if(start == end){
return;
}
//2. 提取中点
int middle = start + (end - start)/2;
//3. 前一半排序
MergeSort(nums,start,middle);
//4. 后一半排序
MergeSort(nums,middle+1,end);
//5. 合并
Merge(nums,start,middle,end);
}
private static void Merge(int[] nums, int start, int middle, int end) {
//1. 创建end - start + 1长度的数组,存放本次合并结果
int[] temp = new int[end - start + 1];
int index = 0;
//2. 提取左半和右半的起点
int index1 = start;
int index2 = middle+1;
//3. while合并
while(index1 <= middle && index2 <= end){
if(nums[index1] < nums[index2]){
temp[index++] = nums[index1++];
}else{
temp[index++] = nums[index2++];
}
}
//4. 补录
while(index1 <= middle){
temp[index++] = nums[index1++];
}
while(index2 <= end){
temp[index++] = nums[index2++];
}
//5. 重新存回到nums,从start开始
for(int i=0;i
5. 快排(不稳定):
- 时间复杂度:Onlogn,最坏On2
- 空间复杂度:O(logn) //pivot断点空间O(logn)
- 最好情况O(n*logn),二叉树。——Partition函数每次恰好能均分序列,其递归树的深度就为.log2n.+1(.x.表示不大于x的最大整数),即仅需递归log2n次; 最坏情况O(n^2),单链表,每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序,如果用数画出来,得到的将会是一棵单斜树,也就是说所有所有的节点只有左(右)节点的树
- 不稳定:因为快排要进行交换,可能会改变两个相等元素的象对位置
- 具体实现
private static void quickSort(int[] nums, int start, int end) {
//1. 判断越界
if(start > end){
return;
}
//2. 提取基准数
int pivot = getPivot(nums,start,end);
//3. 递归左半部分
quickSort(nums,start,pivot-1);
//4. 递归右半部分
quickSort(nums,pivot+1,end);
}
private static int getPivot(int[] nums, int start, int end) {
//1. 提取基准数为nums[start]
int pivot = nums[start];
//2. 左右指针
int left = start;
int right = end;
//3. while循环
while(left <= right){
while (left <= right && nums[left] <= pivot){
left++;
}
while(left <= right && nums[right] > pivot){
right--;
}
if(left < right){
swap(nums,left,right);
}
}
//4. 基准数归位
swap(nums,start,right);
return right;
}
6. 快速选择quickselect:TOPK找单一元素问题首选,递归排序结束后,nums[0]就是TOPK
- 时间复杂度:On
- 空间复杂度:On
private static void quickSort(int[] nums, int start, int end,int k) {
//1. 判断越界
if(start == end){
return;
}
//2. 提取基准数
int pivot = nums[start];
//3. 左右指针
int left = start;
int right = end;
//3. while循环
while(left <= right){
while (left <= right && nums[left] < pivot){
left++;
}
while (left <= right && nums[right] > pivot){
right--;
}
if(left <= right){
swap(nums,left,right);
left++;
right--;
}
}
//4. 继续递归
if(start <= k && k <= right){
quickSort(nums,start,right,k);
}
if(left <= k && k <= end){
quickSort(nums,left,end,k);
}
}
7. 堆排(O(nlogn))(不稳定)
- 时间复杂度:Onlogn
- 空间复杂度:O(1)
- 基本原理:是将待排序记录看作完全二叉树,可以建立大根堆或小根堆。
- 以大根堆为例,
- 建堆:提取当前下标,while true循环交换父节点和当前下标。如果能够交换,将当前下标替换为父节点下标
- 弹出:暂存根节点。根节点与最后一个节点交换。while true循环。左右节点下标。如果左节点越界,break。否则将maxIndex设置为left,maxIndex与right比较,如果小,交换。交换后的maxIndex与根节点比较,如果比根节点大,交换,将跟节点下标替换为maxIndex
//构建堆结构
for(int i=0;i=0;i--){
nums[i] = HeapPoll(nums,size--);
}
for(int i=0;i= index){
break;
}
//先暂存maxIndex
int maxIndex = left;
//2. 左右节点值比较
if(right < index && nums[maxIndex] < nums[right]){
maxIndex = right;
}
//3. 本次最大值与根节点比较
if(nums[curIndex] < nums[maxIndex]){
swap(nums,curIndex,maxIndex);
}else{
break;
}
//5. 将根节点下标替换为maxIndex
curIndex = maxIndex;
}
return result;
}
private static void createHeap(int[] nums, int index) {
//1. 提取index
int curIndex = index;
//2. while循环
while(curIndex > 0){
//父节点下标
int parentIndex = (curIndex-1)/2;
//如果父节点比当前节点小,交换。否则break循环
if(nums[parentIndex] < nums[curIndex]){
swap(nums,parentIndex,curIndex);
}else{
break;
}
//提取父节点下标为当前节点下标
curIndex = parentIndex;
}
}
Arrays.sort()内部实现
- size小于60,用插排
- size大于60,用快排或归并
海量数据处理:https://github.com/doocs/advanced-java
1. 在大量的URL中找出相同的URL
- 题目:给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。
- 分析:
- 内存不足,分治策略,遍历大文件,将结果存储到1000个小文件中
- 解法:
- 遍历文件a,对URL求hash取余:hash(url) % 1000,分成1000个数据。
- 遍历文件b,与文件a做相同处理。
- 遍历a,将url存入HashSet,然后遍历b,将url存入HashSet。HashSet中数据就是相同的URL
- 总结
- 内存不足,分治策略
- 遍历大文件,对URL进行hash取余,将结果存储到1000个小文件中,每个小文件大小不超过内存限制
- 分别遍历小文件,使用HashSet去重
2. 大量数据找高频词
- 题目:有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)。
- 分析:
- 内存不足,分治策略,遍历大文件,将结果存储到5000个小文件中
- 解法:
- 遍历大文件,对词求hash取余:hash(x) % 5000,分成5000个小文件。
- 遍历这5000个小文件,使用HashMap统计词出现次数。
- 大根堆,容量为100。依次遍历小文件,比较HashMap.get(b)-HashMap.get(a),存入keySet,然后依次弹出即可
- 总结
- 内存不足,分治策略,每个小文件不超过1MB
- 遍历大文件,求hash取余,拆分成小文件
- 使用HashMap统计出现次数
- 大根堆比较出现次数:Map.get(b)-Map.get(a)。存入keySet,弹出
3. 如何找出某一天访问百度网站最多的 IP?
- 题目:现有海量日志数据保存在一个超大文件中,该文件无法直接读入内存,要求从中提取某天访问百度次数最多的那个 IP。
- 分析
- 内存不足,分治策略,遍历大文件,将结果存储到x个小文件中
- 解法
- 遍历大文件,对ip求hash取余,hash(ip) % x,分成x个小文件
- 遍历x个小文件,使用HashMap统计出现次数
- 遍历keySet,求出max值即可
- 总结
- 内存不足,分治策略,每个小文件不超过限制
- 遍历小文件,求hash取余,拆分成小文件
- 使用HashMap统计出现次数
- 遍历keySet,求出max
4. 如何在大量的数据中找出不重复的整数?
- 题目:在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。
- 分析
- 内存不足,分治策略,将大文件拆分为小文件
- bit数组位图。一个或多个bit来标记某个元素的值,bit空间小。
- 例如:对于0-7之间的数组(6,5,4,2,1,3)来说,可以创建一共8bit的数组[0,0,0,0,0,0,0,0]
- 遍历数组,6,对应bit[5]标为1,遇到5,对应bit[4]标为1,依次标记。最终bit数组:[0,1,1,1,1,1,1,0]。遍历
- 本题:可以用两个bit数组来标记。
- 00:没出现过
- 01:出现一次,目标值
- 10:出现多次
- 解法:
- 分治策略同123题
- 位图法
- 2.5亿个整数,1个整数4位,4 * 8bit = 32bit。需要的bit数组大小为2的32bit * 2b = 1GB。
- 遍历bit数组,将01位整数输出
- 总结
- 内存不足,分治策略
- 大数据找重复、不重复、目标值。用位图
5. 如何在大量的数据中判断一个数是否存在?
- 题目:给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?
- 分析
- 内存不足,分治策略,将大文件拆分成小文件2
- bit数组位图法。
- 解法
- 40亿个整数,创建bit数组,bit数组大小位40亿 * 2bit = 500MB。存在着设置为1,不存在设置为0。
- 总结
- 内存不足,分治策略
- 大数据找重复值、不重复值、目标值。用位图
6. 如何查询最热门的查询串?
- 题目:假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。
- 分析:
- 每个字符串255B,1000W个共2.55G。内存不足,分治策略
- 统计出现次数,可以用HashMap
- 解法
- 分治策略,划分为多个小文件,求出每个小文件中出现次数前10的字符串(使用HashMap),通过大根堆求出出现次数前10的字符串
- hashMap。去重后不超过300W个,即大约需要700MB。直接使用HashMap统计
- 总结
- 内存不足,分治策略
- 大数据找重复字符串,重复度高的情况下,可以考虑使用HashMap直接统计
7. 如何统计不同电话号码的个数?
- 题目:已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。
- 分析
- 数据重复问题,考虑bit位图
- 解法
- 8位电话号码,个数位10的8次方个,1亿个。1亿个Bit,大小100MB。
- 创建长度1亿的bit数组。遍历电话号码,存在为1。统计1的个数
- 总结
- 数据重复问题,考虑位图
8. 如何找出排名前 500 的数?
- 题目:有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?
- 分析
- 大数据TOPk问题,考虑堆排
- 20个数组,每个数组有500个元素,且有序,可以看作是二维数组。
- 解法
- 创建一个比较器,用于在建堆时将每个数组最大的值存入堆。
- 建立一个大小为20的大根堆,遍历二维数组的行,假设每一行数组是降序,我们可以将每一行的最大值入堆
- while循环弹出堆顶,用一个大小500的数组接收,然后index+1将本行的后续元素入堆,重复500次,直到填满数组为止
- 详细代码
import lombok.Data;
import java.util.Arrays;
import java.util.PriorityQueue;
/**
* @author https://github.com/yanglbme
*/
@Data
public class DataWithSource implements Comparable {
/**
* 数值
*/
private int value;
/**
* 记录数值来源的数组
*/
private int source;
/**
* 记录数值在数组中的索引
*/
private int index;
public DataWithSource(int value, int source, int index) {
this.value = value;
this.source = source;
this.index = index;
}
/**
*
* 由于 PriorityQueue 使用小顶堆来实现,这里通过修改
* 两个整数的比较逻辑来让 PriorityQueue 变成大顶堆
*/
@Override
public int compareTo(DataWithSource o) {
return Integer.compare(o.getValue(), this.value);
}
}
class Test {
public static int[] getTop(int[][] data) {
int rowSize = data.length;
int columnSize = data[0].length;
// 创建一个columnSize大小的数组,存放结果
int[] result = new int[columnSize];
PriorityQueue maxHeap = new PriorityQueue<>();
for (int i = 0; i < rowSize; ++i) {
// 将每个数组的最大一个元素放入堆中
DataWithSource d = new DataWithSource(data[i][0], i, 0);
maxHeap.add(d);
}
int num = 0;
while (num < columnSize) {
// 删除堆顶元素
DataWithSource d = maxHeap.poll();
result[num++] = d.getValue();
if (num >= columnSize) {
break;
}
d.setValue(data[d.getSource()][d.getIndex() + 1]);
d.setIndex(d.getIndex() + 1);
maxHeap.add(d);
}
return result;
}
public static void main(String[] args) {
int[][] data = {
{29, 17, 14, 2, 1},
{19, 17, 16, 15, 6},
{30, 25, 20, 14, 5},
};
int[] top = getTop(data);
System.out.println(Arrays.toString(top)); // [30, 29, 25, 20, 19]
}
}
9. 按照query的频度排序
- 题目:有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。
- 分析
- 如果重复度高,直接用HashMap可能不会超内存。如果重复度不高,还是考虑分治策略
- 解法
- 重复度高,使用HashMap直接统计出现次数,按照出现次数排序
- 重复度不高,分治,将大文件拆分成小文件,hash取余,使用HashMap分别统计小文件的query出现次数,然后进行排序(如果内存不足,可以使用归并排序)
- 总结
- 数据出现次数,可以使用HashMap
10. 如何从 5 亿个数中找出中位数?
- 从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 (N+1)/2 个数;当样本数为偶数时,中位数为 第 N/2 个数与第 1+N/2 个数的均值。
- 分析
- 分治法,分为正数和负数。遍历这5亿个数,转换为二进制符号位,如果最高位为1,是负数,存入neg数组,如果最高位位0,是正数,存入pos数组。完成之后,如果neg数组长度为1亿,那么中位数必定在pos数组的第1.5亿个数与后面一个数的平均值
11. 如果一个黑名单网站包含100亿个黑名单网页,每个网页最多占64B,设计一个系统,判断当前的URL是否在这个黑名单当中,要求额外空间不超过30GB,允许误差率为万分之一。
- 分析:假设一个网页黑名单有URL为100亿,每个样本为64B,失误率为0.01%,经过布隆过滤器的公式计算后,需要布隆过滤器大小为25GB,这远远小于使用哈希表的640GB的空间。并且由于是通过hash进行查找的,所以基本都可以在O(1)的时间完成!
- 解法
- 导入布隆过滤器依赖
- 创建布隆过滤器,遍历存入,统计误判次数
- 根据误判次数计算误差率