八大排序算法及海量数据处理

排序算法

基础排序,时间复杂度O(n2)

  1. 直接插入排序(稳定)
  2. 冒泡排序(稳定)
  3. 选择排序(不稳定)

进阶排序,时间复杂度O(nlogn)

  1. 快排(不稳定)
  2. 归并(稳定)
  3. 堆排(不稳定)

1. 直接插入排序(稳定):从i=1开始遍历,提取nums[i]作为标准,排序[insertIndex,i-1]区间,排序完成之后,将nums[i]插入到insertIndex+1位置

  1. 时间复杂度:O(n2),最好On
  2. 空间复杂度:O(1)
  3. 稳定:在排序后的序列后面插入,就算是相等,相对位置没有发生变化,稳定
  4. 基本实现
    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],前后比较,如果前比后大,交换

  1. 时间复杂度:On2,最好On
  2. 空间复杂度:O(1)
  3. 稳定:只是交换相邻两个元素,如果存在两个相等的相邻元素,不交换,相对位置没有发生变化。因此是稳定的
  4. 具体实现
    public void bubbleSort(int[] nums){
        if(nums.length < 2){
            return;
        }

        for(int i=0;i nums[j+1]){
                    swap(nums,j,j+1);
                }
            }
        }
    }
  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的值

  1. 时间复杂度;O(n2)
  2. 空间复杂度:O(1)
  3. 不稳定:因为要提取基准值作为判断标准,如果比基准值小,基准值和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])

  1. 时间复杂度:
    1. 平均O(nlogn)
    2. 最好O(nlogn)
    3. 最坏O(nlogn)
  2. 空间复杂度:O(n)
  3. 稳定:相等元素在合并时并不会改变相对位置。稳定
  4. 具体实现:
    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. 快排(不稳定):

  1. 时间复杂度:Onlogn,最坏On2
  2. 空间复杂度:O(logn) //pivot断点空间O(logn)
  3. 最好情况O(n*logn),二叉树。——Partition函数每次恰好能均分序列,其递归树的深度就为.log2n.+1(.x.表示不大于x的最大整数),即仅需递归log2n次; 最坏情况O(n^2),单链表,每次划分只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序,如果用数画出来,得到的将会是一棵单斜树,也就是说所有所有的节点只有左(右)节点的树
  4. 不稳定:因为快排要进行交换,可能会改变两个相等元素的象对位置
  5. 具体实现
    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

  1. 时间复杂度:On
  2. 空间复杂度: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))(不稳定)

  1. 时间复杂度:Onlogn
  2. 空间复杂度:O(1)
  3. 基本原理:是将待排序记录看作完全二叉树,可以建立大根堆或小根堆。
  4. 以大根堆为例,
    1. 建堆:提取当前下标,while true循环交换父节点和当前下标。如果能够交换,将当前下标替换为父节点下标
    2. 弹出:暂存根节点。根节点与最后一个节点交换。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()内部实现

  1. size小于60,用插排
  2. size大于60,用快排或归并

海量数据处理:https://github.com/doocs/advanced-java

1. 在大量的URL中找出相同的URL

  1. 题目:给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。
  2. 分析:
    1. 内存不足,分治策略,遍历大文件,将结果存储到1000个小文件中
  3. 解法:
    1. 遍历文件a,对URL求hash取余:hash(url) % 1000,分成1000个数据。
    2. 遍历文件b,与文件a做相同处理。
    3. 遍历a,将url存入HashSet,然后遍历b,将url存入HashSet。HashSet中数据就是相同的URL
  4. 总结
    1. 内存不足,分治策略
    2. 遍历大文件,对URL进行hash取余,将结果存储到1000个小文件中,每个小文件大小不超过内存限制
    3. 分别遍历小文件,使用HashSet去重

2. 大量数据找高频词

  1. 题目:有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)。
  2. 分析:
    1. 内存不足,分治策略,遍历大文件,将结果存储到5000个小文件中
  3. 解法:
    1. 遍历大文件,对词求hash取余:hash(x) % 5000,分成5000个小文件。
    2. 遍历这5000个小文件,使用HashMap统计词出现次数。
    3. 大根堆,容量为100。依次遍历小文件,比较HashMap.get(b)-HashMap.get(a),存入keySet,然后依次弹出即可
  4. 总结
    1. 内存不足,分治策略,每个小文件不超过1MB
    2. 遍历大文件,求hash取余,拆分成小文件
    3. 使用HashMap统计出现次数
    4. 大根堆比较出现次数:Map.get(b)-Map.get(a)。存入keySet,弹出

3. 如何找出某一天访问百度网站最多的 IP?

  1. 题目:现有海量日志数据保存在一个超大文件中,该文件无法直接读入内存,要求从中提取某天访问百度次数最多的那个 IP。
  2. 分析
    1. 内存不足,分治策略,遍历大文件,将结果存储到x个小文件中
  3. 解法
    1. 遍历大文件,对ip求hash取余,hash(ip) % x,分成x个小文件
    2. 遍历x个小文件,使用HashMap统计出现次数
    3. 遍历keySet,求出max值即可
  4. 总结
    1. 内存不足,分治策略,每个小文件不超过限制
    2. 遍历小文件,求hash取余,拆分成小文件
    3. 使用HashMap统计出现次数
    4. 遍历keySet,求出max

4. 如何在大量的数据中找出不重复的整数?

  1. 题目:在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。
  2. 分析
    1. 内存不足,分治策略,将大文件拆分为小文件
    2. bit数组位图。一个或多个bit来标记某个元素的值,bit空间小。
      1. 例如:对于0-7之间的数组(6,5,4,2,1,3)来说,可以创建一共8bit的数组[0,0,0,0,0,0,0,0]
      2. 遍历数组,6,对应bit[5]标为1,遇到5,对应bit[4]标为1,依次标记。最终bit数组:[0,1,1,1,1,1,1,0]。遍历
      3. 本题:可以用两个bit数组来标记。
        1. 00:没出现过
        2. 01:出现一次,目标值
        3. 10:出现多次
  3. 解法:
    1. 分治策略同123题
    2. 位图法
      1. 2.5亿个整数,1个整数4位,4 * 8bit = 32bit。需要的bit数组大小为2的32bit * 2b = 1GB。
      2. 遍历bit数组,将01位整数输出
  4. 总结
    1. 内存不足,分治策略
    2. 大数据找重复、不重复、目标值。用位图

5. 如何在大量的数据中判断一个数是否存在?

  1. 题目:给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?
  2. 分析
    1. 内存不足,分治策略,将大文件拆分成小文件2
    2. bit数组位图法。
  3. 解法
    1. 40亿个整数,创建bit数组,bit数组大小位40亿 * 2bit = 500MB。存在着设置为1,不存在设置为0。
  4. 总结
    1. 内存不足,分治策略
    2. 大数据找重复值、不重复值、目标值。用位图

6. 如何查询最热门的查询串?

  1. 题目:假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。
  2. 分析:
    1. 每个字符串255B,1000W个共2.55G。内存不足,分治策略
    2. 统计出现次数,可以用HashMap
  3. 解法
    1. 分治策略,划分为多个小文件,求出每个小文件中出现次数前10的字符串(使用HashMap),通过大根堆求出出现次数前10的字符串
    2. hashMap。去重后不超过300W个,即大约需要700MB。直接使用HashMap统计
  4. 总结
    1. 内存不足,分治策略
    2. 大数据找重复字符串,重复度高的情况下,可以考虑使用HashMap直接统计

7. 如何统计不同电话号码的个数?

  1. 题目:已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。
  2. 分析
    1. 数据重复问题,考虑bit位图
  3. 解法
    1. 8位电话号码,个数位10的8次方个,1亿个。1亿个Bit,大小100MB。
    2. 创建长度1亿的bit数组。遍历电话号码,存在为1。统计1的个数
  4. 总结
    1. 数据重复问题,考虑位图

8. 如何找出排名前 500 的数?

  1. 题目:有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?
  2. 分析
    1. 大数据TOPk问题,考虑堆排
    2. 20个数组,每个数组有500个元素,且有序,可以看作是二维数组。
  3. 解法
    1. 创建一个比较器,用于在建堆时将每个数组最大的值存入堆。
    2. 建立一个大小为20的大根堆,遍历二维数组的行,假设每一行数组是降序,我们可以将每一行的最大值入堆
    3. while循环弹出堆顶,用一个大小500的数组接收,然后index+1将本行的后续元素入堆,重复500次,直到填满数组为止
  4. 详细代码
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的频度排序

  1. 题目:有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。
  2. 分析
    1. 如果重复度高,直接用HashMap可能不会超内存。如果重复度不高,还是考虑分治策略
  3. 解法
    1. 重复度高,使用HashMap直接统计出现次数,按照出现次数排序
    2. 重复度不高,分治,将大文件拆分成小文件,hash取余,使用HashMap分别统计小文件的query出现次数,然后进行排序(如果内存不足,可以使用归并排序)
  4. 总结
    1. 数据出现次数,可以使用HashMap

10. 如何从 5 亿个数中找出中位数?

  1. 从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 (N+1)/2 个数;当样本数为偶数时,中位数为 第 N/2 个数与第 1+N/2 个数的均值。
  2. 分析
    1. 分治法,分为正数和负数。遍历这5亿个数,转换为二进制符号位,如果最高位为1,是负数,存入neg数组,如果最高位位0,是正数,存入pos数组。完成之后,如果neg数组长度为1亿,那么中位数必定在pos数组的第1.5亿个数与后面一个数的平均值

11. 如果一个黑名单网站包含100亿个黑名单网页,每个网页最多占64B,设计一个系统,判断当前的URL是否在这个黑名单当中,要求额外空间不超过30GB,允许误差率为万分之一。

  1. 分析:假设一个网页黑名单有URL为100亿,每个样本为64B,失误率为0.01%,经过布隆过滤器的公式计算后,需要布隆过滤器大小为25GB,这远远小于使用哈希表的640GB的空间。并且由于是通过hash进行查找的,所以基本都可以在O(1)的时间完成!
  2. 解法
    1. 导入布隆过滤器依赖
    2. 创建布隆过滤器,遍历存入,统计误判次数
    3. 根据误判次数计算误差率

你可能感兴趣的:(八大排序算法及海量数据处理)