第一章:基础算法

第一章:基础算法

排序

快速排序

分治算法

  1. 确定分界点x(有三种方法,a、直接取左边界q[l],b、取中间值q[$(l +r)/ 2$], c、随机)
  2. 调整区间,将区间划分为两段,左边所有的数都是小于等于x,右边所有的数大于等于x
  3. 递归处理左右两段
public void quickSort(int q[], int l, int r){
    if (l >= r) return;
		//数值x为分界数
  	//移动策略:左右两个指针都是先分别往右、往左移动,再进行比较,所以这里给i和j的初始赋值是l-1,和 r+1
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
   //while循环的目的在于使得下标i左边所有的数都是小于x的,j下标右边所有的数都是大于x的
    while (i < j){
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) {
					//交换去q[i] 与 q[j]
          q[i] = q[i] ^ q[j];
          q[j] = q[i] ^ q[j];
          q[i] = q[i] ^ q[j];
        }
    }
  	//递归
    // 注意:这里j不能换成i
    quickSort(q, l, j);
  	quickSort(q, j + 1, r);
}

归并排序

  1. 确定分界点:$mid = (l +r) / 2$
  2. 递归排序 leftright,使得leftright 有序
  3. 归并,将leftright 两个有序的数列合并成一个有序的数组
/**
* 归并排序代码模板
* @param q 待排序的模板
* @param l 起始点下标
* @param r 终点下标
*/
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));
    }

    /**
     * 堆排序,(大堆根)从小到大排序
     *
     * @param array
     */
    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 为下标
bool check(int x) {
  /* ... */
} 

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int binarySearch01(int l, int r){
    while (l < r)
    {
        //区别
        int mid = l + r >> 1;
        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;
        if (check(mid)) l = mid; //check()判断mid是否满足性质
        else r = mid - 1;
    }
    return l;
}

练习:789. 数的范围

浮点数二分

//检查x是否满足某种性质
bool 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 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 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 segs){
  	//存储结果
    ArrayList 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;
        }
    }
}

你可能感兴趣的:(第一章:基础算法)