第一章:基础算法
排序
选择排序
思路:首先是找到数组中最小的那个数组,其次,将它和数组的第一个元素进行交换位置(如果第一个元素就是最小元素那么就和自己交换)。
再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到整个数组排序。
具有次交换和次比较
public void selectSort(int[] array) {
int length = array.length;
for (int i = 0; i < length; i++) {
// 注意:必须是下标,才能在后续达到交换数值的要求
int min = i;
for (int j = i + 1; j < length; j++) {
if (array[min] > array[j]) min = j;
}
//交换
int tmp = array[min];
array[min] = array[i];
array[i] = tmp;
}
}
插入排序
思路:将后面的数插入到之前已经排好序的数中的合适位置,具体的方式是依次与之前排序好的数进行比较交换。
public void insertSort(int[] array) {
int length = array.length;
for (int i = 1; i < length; i++) {
// 和选择排序不同的是:j 从 i 开始往前移动
for (int j = i; j > 0; j--) {
if (array[j] < array[j - 1]) {
//后移的本质,在于交换
int tmp = array[j];
array[j] = array[j - 1];
array[j - 1] = tmp;
}
}
}
}
不需要交换的插入排序,在插入排序的实现中使较大元素右移一位只需要访问一次数组。相对于交换,可以提升插入排序的速度
public static void sort2(int[] array) {
int length = array.length;
for (int i = 1; i < length; i++) {
//
int tmp = array[i];
int j = i - 1;
while (j >= 0 && tmp < array[j]) {
array[j + 1] = array[j];
j--;
}
array[j + 1] = tmp;
}
}
希尔排序
希尔排序是一种基于插入排序的快速的排序算法。
对于大规模乱序数组插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
h从N/3开始递减至1,我们可以称这个序列称为递增序列。h则是每次被排序数的下标的间隔
public static void sort(int[] array) {
int length = array.length;
int h = 1;
while (h < length / 3) h = h * 3 + 1; //1, 4, 13, 40,121 ...
while (h >= 1) {
// 将数组变为h有序
for (int i = h; i < length; i++) {
for (int j = i; j >= h; j -= h) {
if (array[j] < array[j - h]) {
int tmp = array[j];
array[j] = array[j - h];
array[j - h] = tmp;
}
}
}
h = h / 3; //缩小h的规模
}
}
因为希尔排序是基于插入排序,所以也存在不需要交换的希尔排序
public static void sort2(int[] array) {
int length = array.length;
int h = 1;
while (h < length / 3) h = h * 3 + 1;
while (h >= 1) {
for (int i = h; i < length; i++) {
int tmp = array[i];
// 每次与位于 i-h 的值进行比较
int j = i - h;
while (j >= 0 && tmp < array[j]) {
array[j + h] = array[j];
j -= h;
}
array[j + h] = tmp;
}
h = h / 3;
}
}
快速排序
分治算法
- 确定分界点x(有三种方法,a、直接取左边界q[l],b、取中间值q[], c、随机)
- 调整区间,将区间划分为两段,左边所有的数都是小于等于x,右边所有的数大于等于x
- 递归处理左右两段
public void quickSort(int arr[], int l, int r){
if (l >= r) return;
//数值x为分界数
//移动策略:左右两个指针都是先分别往右、往左移动,再进行比较,所以这里给i和j的初始赋值是l-1,和 r+1
int i = l - 1, j = r + 1, x = arr[l + r >> 1]; // + 的优先级大于 >>
//while循环的目的在于使得下标i左边所有的数都是小于x的,j下标右边所有的数都是大于x的
while (i < j){ //可能存在某种情况,i > j;例如arr[] = {3, 1, 2, 3, 5};i,j 分别指向两端,会存在i>j的情况
do i ++ ; while (arr[i] < x);
do j -- ; while (arr[j] > x);
if (i < j) {
//交换去arr[i] 与 arr[j]
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
}
//递归
//注意:这里j不能换成i,否则会产生边界问题
quickSort(arr, l, j);
quickSort(arr, j + 1, r);
}
归并排序
- 确定分界点:
- 递归排序
left和right,使得left和right有序 - 归并,将
left和right两个有序的数列合并成一个有序的数组
void mergeSort(int q[], int l, int r){
if (l >= r) return;
//中间结点下标
int mid = l + r >> 1; // + 的优先级大于 >>
//递归排序
mergeSort(q, l, mid);
mergeSort(q, mid + 1, r);
//归并
int k = 0, i = l, j = mid + 1;
//比较left 和 right 的值,将较小的值放到临时数组tmp中
while (i <= mid && j <= r) {
if (q[i] < q[j]) {
tmp[k ++ ] = q[i ++ ];
} else {
tmp[k ++ ] = q[j ++ ];
}
}
//rigth 部分已经遍历完了,但是 left 还剩余部分,则将left 剩下的数放到tmp中
while (i <= mid) {tmp[k ++ ] = q[i ++ ];}
//left 部分已经遍历完了,但是 right 还剩余部分,则将right 剩下的数放到tmp中
while (j <= r) {tmp[k ++ ] = q[j ++ ];}
//将已经排序好的临时变量tmp,复制到数组q中
for (i = l, j = 0; i <= r; i ++, j ++ ) {q[i] = tmp[j];}
}
堆排序
/**
* 堆分两种:
* 1、小根堆
* 此代码思路:
* 先将数组视为完全二叉树,将该完全二叉树转化为小根堆。在小根堆的基础上进行排序
* 但是根据上课老师的思路:
* 实现堆还存在另外一种思路:
* 那就是边遍历数组边新建小根堆
* 2、大根堆
* 思路同上
*/
import java.util.*;
public class Sort {
public static void main(String[] args) {
int[] arr2 = new int[]{3, 2, 3, 1, 2, 4, 5, 5, 6};
heapSort(arr2);
System.out.println(Arrays.toString(arr2));
}
/**
* 堆排序,(大堆根)从小到大排序
*/
public static void heapSort(int[] array) {
//1、把无序数组构建成最大堆
/*
* 注意:
* for循环中i的值为什么是从(array.length / 2)开始
* 根据堆排序的思想,我们知道要从最后一个非叶子节点开始进行下沉调整,然后向前遍历
* 所以:i = array.length / 2 -1 ,且 i--
* */
for (int i = array.length / 2 - 1; i >= 0; i--) {
downAdjust(array, i, array.length - 1);
}
//2、循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
for (int i = array.length - 1; i > 0; i--) {
//最后一个元素和第一个元素进行交换
int temp = array[i];
array[i] = array[0];
array[0] = temp;
//"下沉"调整最大堆
downAdjust(array, 0, i - 1);
}
}
/**
* "下沉"调整
* @param array 待调整堆
* @param parentIndex 要下沉的父节点
* @param endIndex 堆的有效大小
*/
public static void downAdjust(int[] array, int parentIndex, int endIndex) {
//temp保存父节点值,用于最后的赋值,
int temp = array[parentIndex];
//根据父节点的位置计算出子节点的位置
//下面计算的是左孩子的位置
int childIndex = 2 * parentIndex + 1;
/*
当childIndex < length 成立时,存在左孩子
如果也存在右孩子,先定位到左右孩子中较大的那一项
*/
while (childIndex <= endIndex) {
if (childIndex + 1 <= endIndex && array[childIndex + 1] > array[childIndex]) { //取当前节点中较大的那个
childIndex++;
}
/*
1、若父节点大于孩子节点,则退出当前循环
2、若父节点小于孩子节点,我们一开始想到的就是将父节点与孩子的值进行交换,
但是,我们应该考虑到该函数的作用在于:将父节点下沉到最小的位置上
如果孩子节点还存在着孩子节点,我们暂且称之为孙子节点,
若孙子节点的值是也是大于父节点,那么我们应当将父节点下沉到孙子节点处,
以此类推,直到父节点大于子节点,或是不存在子节点,才结束
3、最后将父子节点值赋值到子节点
*/
if (temp > array[childIndex]) { //tmp 为父节点
//父节点大于左右子节点
break;
} else {
//父节点小于左右子节点
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = 2 * parentIndex + 1;
}
}
array[parentIndex] = temp;
}
}
二分
如果有单调性那么肯定可以二分,如果没单调性,也可以二分。二分的本质是边界,而不是单调性
二分是用于一定有解的题目
整数二分
// 检查x是否满足某种性质
//x 为下标
boolean check(int x) {
/* ... */
}
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:往左走
int binarySearch01(int l, int r){
while (l < r) {
//区别
int mid = l + r >> 1; //mid 为奇数数组中间位置,为偶数数组的中间靠前的位置
if (check(mid)) r = mid; //check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:往右走
int binarySearch02(int l, int r){
while (l < r){
//区别
int mid = l + r + 1 >> 1;//mid 为奇数数组中间位置,为偶数数组的中间靠后的位置
if (check(mid)) l = mid; //check()判断mid是否满足性质
else r = mid - 1;
}
return l;
}
练习:789. 数的范围
浮点数二分
//检查x是否满足某种性质
boolean check(double x) {
/* ... */
}
double binarySearch03(double l, double r){
const double eps = 1e-8; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
前缀和
一维前缀和
//下标一定从1开始
S[0] = 0;//可以帮助处理边界
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
二维前缀和
//S[i, j] = 第i行j列格子左上部分所有元素的和
//注意:数组S下标都是从1开始,以避免边界问题
S[i,j] = S[i - 1, j] + S[i, j - 1] - S[i - 1, j - 1] + a[i][j]
//以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
//因为S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] 多减了一个S[x1 - 1, y1 - 1], 所以最后加上了
//S[x1 - 1, y1 - 1]
差分
一维差分
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
双指针算法
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
//O(n^2)
}
}
双指针算法的核心在于优化暴力搜索,将其时间复杂度O(n^2),转化成O(n)
//i,j均为下标
for (int i = 0, j = 0; i < n; i ++ ){
//check(i,j) 检查i,j之间存在的某种关系是否成立
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
//常见问题分类:
// (1) 对于一个序列,用两个指针维护一段区间
// (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
位运算
求n的二进制数的第k位数字: n >> k & 1
返回n的最后一位1(最右边的1)的二进制数:lowbit(n) = n & -n == n & (~n + 1)
举例:
x = 10100 lowbit(x) = 100
x = 10010 lowbit(x) = 10
离散化
ArrayList<Integer> alls = new ArrayList<>(); // 存储所有待离散化的值
Collections.sort(all); // 将所有值排序,从小到大
alls.subList(0, unique(alls)); //提取非重复元素
// 二分求出x对应的离散化的值
int find(int x){ // 找到第一个大于等于x的位置
int l = 0, r = alls.size() - 1;
while (l < r){
int mid = l + r >> 1;
if (alls.get(mid) >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}
//去重
//将不重复的数放到List的前部分,返回不重复元素的最右边的下标
//注意:此时的list已经被排序过了
static int unique(List<Integer> list) {
int j = 0;
for (int i = 0; i < list.size(); i++) {
if (i == 0 || list.get(i) != list.get(i - 1)) {
list.set(j, list.get(i));
j++;
}
}
return j;
}
区间合并
// 将所有存在交集的区间合并
void merge(ArrayList<Pairs> segs){
//存储结果
ArrayList<Pairs> res;
//按照Pairs默认的排序方法进行排序
Collections.sort(seg);
//st为起始, ed为结束
//我们维护的临时区间的左右位置
int st = -2e9, ed = -2e9; // -2e9: 2 * 10 ^ 9;
//遍历
for (Pairs seg : segs)
//维护的区间的最右边位置小于seg的最左边的位置,那么则把该区间加入到答案中
if (ed < seg.first){
if (st != -2e9) {
res.add(new Pairs(st, ed));
}
//更新临时区间
st = seg.first, ed = seg.second;
}else {
//存在交集,更新区间最右边
ed = Math.max(ed, seg.second);
}
//防止输入数组里是没有任何区间的
if (st != -2e9) res.add(new Pairs(st, ed));
segs = res;
}
//存储区间的start和end
class Pairs implements Comparable {
int first;
int second;
public Pairs(int first, int second) {
this.first = first;
this.second = second;
}
//添加从小到大排序的方法,当first相同时,则按照second从小到大排序
//那么Collection的排序会按照该方法排序
@Override
public int compareTo(Object o) {
Pairs pairs = (Pairs) o;
if (pairs.first < first)
return 1;
else if (pairs.first == first) {
if (pairs.second < second)
return 1;
else {
return -1;
}
} else {
return -1;
}
}
}