面向Google刷题 · 二阶段:排序

63 阅读6分钟

比较器(对象排序)

Comparable

具体写的时候总是忘记怎么是升序、降序
总结来看按顺序写就是升序,如:(o1, o2) -> o1.age - o2.age

// 需要实现接口,不能灵活修改排序方式,适合不变的排序
class People implements Comparable<People> {
    int age;
    String name;
    public People(int age, String name) {
        this.age = age;
        this.name = name;
    }
    @Override
    public int compareTo(People o) {
        // 溢出风险:Math.addExact()/Math.subtractExact()
        return this.age - o.age;
        // return o.age - this.age; 降序
    }
}

Comparator

与有序集合一起使用(List、PriorityQueue、TreeSet)

compare

ArrayList<People> list = new ArrayList<>();
list.sort(new Comparator<People>() {
    @Override
    public int compare(People o1, People o2) {
        return o2.age - o1.age; // 降序
    }
});
// lambda 写法
// list.sort((o1, o2) -> o2.age - o1.age);
// 字符串实现了compareTo方法
list.sort(new Comparator<People>() {
    @Override
    public int compare(People o1, People o2) {
        return o1.getName().compareTo(o2.getName()); // 按name升序
    }
}

comparing / thenComparing / reversed / reverseOrder

// 按age降序排列
list.sort(Comparator.comparing(People::getAge).reversed());
// thenComparing可以往后面加,适用多种排序规则
// 当第一个规则无法分出顺序时用下一个规则判断

排序算法

O(n^2)

冒泡排序

水中的泡泡总是最大的先浮出水面

重复比较相邻元素,若顺序错误就交换它们,每一轮会将最大(或最小)的元素“浮”到数组末尾

public void bubbleSort(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        for (int j = 0; j < nums.length - i - 1; j++) {
            if (nums[j] > nums[j + 1]) {
                int tem = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = tem;
            }
        }
    }
}

选择排序

每次从未排序部分选择最小(或最大)的元素,放到已排序部分的末尾

public void selectedSort(int[] nums) {
    // 控制循环次数
    for (int i = 0; i < nums.length - 1; i++) {
        for (int j = i + 1; j < nums.length; j++) {
            // 保证nums[i]最小,升序
            if (nums[j] < nums[i]) {
                int tem = nums[i];
                nums[i] = nums[j];
                nums[j] = tem;
            }
        }
    }
}

插入排序

打扑克牌时每拿到一张牌就插入合适的位置

将数组分为已排序和未排序两部分,每次从未排序部分取出一个元素,插入到已排序部分的合适位置

public void insertSort(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        // [0, i]已经有序,找到 i + 1 应该存放的位置
        for (int j = i + 1; j > 0; j--) {
            if (nums[j] > nums[j - 1]) {
                // 已经有序,结束遍历
                break;
            }
            int tem = nums[j];
            nums[j] = nums[j - 1];
            nums[j - 1] = tem;
        }
    }
}

O(n log n)

归并排序

将一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并成一个有序数组

public void mergeSort(int[] nums) {
    mergeSort(nums, 0, nums.length - 1);
}

public void mergeSort(int[] nums, int low, int high) {
    if (low < high) {
        int mid = low + (high - low) / 2;
        mergeSort(nums, low, mid);
        mergeSort(nums, mid + 1, high);
        
        merge(nums, low, mid + 1, high);
    }
}

public void merge(int[] nums, int low, int mid, int high) {
    // 两个有序数组的起始下标
    int l = low;
    int r = mid;
    int[] tem = new int[high - low + 1];
    int k = 0;
    while (l < mid && r <= high) {
        if (nums[l] <= nums[r]) {
            tem[k++] = nums[l++];
        } else {
            tem[k++] = nums[r++];
        }
    }
    tem[k++] = l < mid ? nums[l++] : nums[r++];
    k = 0;
    while (low <= high) {
        nums[low++] = tem[k++];
    }
}

快速排序

选择一个基准元素,将数组分为两部分,使得左边部分的元素都小于等于基准元素,右边部分的元素都大于基准元素,然后递归地对左右两部分进行排序

public void quickSort(int[] nums) {
    quickSort(nums, 0, nums.length - 1);
}

public void quickSort(int[] nums, int low, int high) {
    if (low < high) {
        int index = partition(nums, low, high);
        
        quickSort(nums, low, index - 1);
        quickSort(nums, index + 1, high);
    }
}

public int partition(int[] nums, int low, int high) {
    int random = low + new Random().nextInt(high - low + 1);
    swap(nums, random, high);
    
    int pivot = nums[high];
    int i = low;
    // 遍历[low, gigh - 1]
    for (int j = low; j < high; j++) {
        if (nums[j] <= pivot) {
            swap(nums, i, j);
            i++;
        }
    }
    swap(nums, i, high);
    return i;
}

public void swap(int[] nums, int x, int y) {
    int tem = nums[x];
    nums[x] = nums[y];
    nums[y] = tem;
}

堆排序

将数组构建成最大堆(或最小堆),然后依次将堆顶元素(最大值或最小值)与堆的最后一个元素交换,再调整剩余元素使其重新满足堆的性质,重复该过程直到整个数组有序

public void heapSort(int[] nums) {
    // 这里不讲堆调整,直接用现成的
    PriorityQueue<Integer> queue = new PriorityQueue<>(Comparator.reverseOrder());
    for (int num : nums) {
        queue.add(num);
    }

    int i = arr.length - 1;
    while (!queue.isEmpty()) {
        nums[i--] = queue.poll();
    }
}

希尔排序

将原始数据分成多个子序列来改善插入排序的性能,先让元素间隔比较大的距离进行排序,随着间隔逐渐减小,最后间隔为 1 时,就相当于进行普通的插入排序

public void shellSort(int[] nums) {
    for (int gap = nums.length / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < nums.length; i++) {
            int j;
            for (j = i; nums[j - gap] > nums[j]; j -= gap) {
                nums[j] = nums[j - gap];
            }
            nums[j] = nums[i];
        }
    }
}

O(n)

计数排序

将输入的数据值转化为键存储在额外开辟的数组空间中。该算法要求输入的数据必须是有确定范围的整数

public void countingSort(int[] nums) {
    // 找出最大值和最小值,确认计数数组大小
    int max = nums[0];
    int min = nums[0];
    for (int num: nums) {
        if (num > max) {
            max = num;
        }
        if (num < min) {
            min = num;
        }
    }
    
    // 创建计数数组
    int[] count = new int[max - min + 1];
    
    // 统计出现次数
    for (int num: nums) {
        count[num - min]++;
    }
    
    // 计算累计计数数组,确定每个元素在结果数组中的位置
    for (int i = 1; i < count.length; i++) {
        count[i] += count[i - 1];
    }
    
    // 创建结果数组
    int[] out = new int[nums.length];
    // 从前往后遍历原数组,找到对应的下标放入
    // 从后往前可以保证稳定性
    for (int i = 0; i < nums.length; i++) {
        out[count[nums[i] - min] - 1] = nums[i];
        count[nums[i] - min]--;
    }
    
    System.arraycopy(out, 0, nums, 0, nums.length);
}

桶排序

将数组分到有限数量的桶子里,每个桶子再分别排序,最后依次把各个桶中的元素合并起来

public void bucketSort(int[] nums) {
    // 找最大最小值
    int max = nums[0];
    int min = nums[0];
    for (int num: nums) {
        if (num > max) {
            max = num;
        }
        if (num < min) {
            min = num;
        }
    }
    // 确定桶个数、大小
    int bucketCount = nums.length;
    if  bucketSize = Math.max(1, (max - min) / bucketCount);
    
    // 创建对应数量的桶
    List<List<Integer>> buckets = new ArrayList<>();
    for (int i = 0; i < bucketCount; i++) {
        buckets.add(new ArrayList<>());
    }
    
    // 将元素放入对应桶内
    for (int num: nums) {
        int index = Math.min(bucketCount - 1, (i - min) / bucketSize);
        buckets.get(index).add(num);
    }
    
    // 桶内排序,合并
    int index = 0;
    for (List<Integer> bucket: buckets) {
        Collections.sort(bucket); // 可任意选择算法,最坏情况下复杂度降为该算法复杂度
        for (int num: bucket) {
            nums[index++] = num;
        }
    }
}

基数排序

将整数按位数切割成不同的数字,然后按每个位数分别比较。它可以从低位到高位(LSD)或者从高位到低位(MSD)进行排序

public void radixSort(int[] nums) {
    // 找最大值,确定要排序的最大位数
    int max = nums[0];
    for (int num: nums) {
        if (num > max) {
            max = num;
        }
    }
    
    // 从低位开始,逐位开始排序
    for (int i = 1; max / i > 0; i *= 10) {
        // i=1个位 i=10十位  i=100百位...
        sort(nums, i);
    }
}

// 其实就是计数排序
public void sort(int[] nums, int exp) {
    int[] out = new int[nums.length];
    int[] count = new int[10];
    
    // 统计该位数上数字出现的次数
    for (int num: nums) {
        count[(num / exp) % 10]++;
    }
    
    // 累计计数
    for (int i = 1; i < 10; i++) {
        count[i] += count[i - 1];
    }
    
    for (int i = nums.length - 1; i >= 0; i--) {
        int digit = (nums[i] / exp) % 10;
        out[count[digit - 1]] = nums[i];
        count[digit - 1]--;
    }
    
    // 完成当前位的排序
    System.arraycopy(out, 0, nums, 0, nums.length);
}