马上各种算法竞赛又要开始了,写这篇博客的主要目的是复习和巩固已经学过的算法,而不是从零开始学习新的算法。
所以对于不会对算法内容进行过多的阐述和讲解,而是以代码展示为主,阅读需要有一定的算法基础。
二分
二分查找(binary search),又称折半查找,是一种搜索算法,适用情况为:
- 有一个区间,有一个判定条件,它们之间满足这样的一个关系:这个区间内存在一个分界点,分界点左边的值均不满足该判定条件,分界点右边的值均满足该判定条件,二分算法就是用于找到这个分界点。
例子1
从一个长度为n的升序数组arr中,找到第一个大于等于x的数,返回它的下标,如果不存在则返回-1。
在这个例子中,区间就是升序数组arr,是一个离散区间,判定条件就是大于等于x,分界点左侧均不满足该条件,分界点右侧均满足该条件,那么代码如下:
public class Main {
static int binarySearch(int[] arr, int n, int x) {
int left = 0, right = n - 1;
while(left < right) {
int middle = (left + right) / 2;
if(arr[middle] >= x) {
// 如果满足判定条件,那么答案一定在 <= middle那边
right = middle;
}
else {
// 如果不满足判定条件,那么答案一定在 > middle那边
left = middle + 1;
}
}
if(arr[left] >= x) {
return left;
}
return -1;
}
public static void main(String[] args) {
int[] arr = new int[] {
1, 2, 3, 4, 5};
System.out.println(binarySearch(arr, 5, 0));
System.out.println(binarySearch(arr, 5, 3));
System.out.println(binarySearch(arr, 5, 6));
}
}
例子2
解方程 x^5 + x^3 + 1 = n,x的值精确到小数点后三位,其中-10^9 <= n <= 10^9
在这个例子中,区间就是x的取值范围,是一个连续区间,没有直接给出来,需要我们自己设定。对于区间的设定,可以比真实取值范围大,但不可以比真实取值范围小,那么我们就直接把区间设置为-10^9 至 10^9即可。
判定条件也没有直接给出来,需要我们自己设定,我们把判定条件设置为x^5 + x^3 + 1 > n,这样分界点左边的值均不满足该条件,右边的值均满足该条件,分界点的值就是x的解。代码如下:
public class Main {
static double funcY(double x) {
return Math.pow(x, 5) + Math.pow(x, 3) + 1;
}
static double binarySearch(double n) {
double right = Math.pow(10, 9);
double left = right * -1;
while(right - left > Math.pow(10, -10)) {
double middle = (left + right) / 2;
if(funcY(middle) > n) {
right = middle;
}
else {
left = middle;
}
}
return left;
}
public static void main(String[] args) {
System.out.printf("%.3f\n", binarySearch(1));
System.out.printf("%.3f\n", binarySearch(2));
System.out.printf("%.3f\n", binarySearch(0));
System.out.printf("%.3f\n", binarySearch(100000000));
}
}
快速排序
其实用处不大,竞赛绝对用不到,面试偶尔用得到,而且比较难写,尤其是边界值下标比较容易搞错,如果没处理好经常会出现无限递归的情况。但毕竟是非常有名的排序方法,所以还是复习一下吧。
快速排序其实是一个递归分治的过程,大概步骤就是这样:
- 随便选取一个值x,对数组中的元素位置进行交换,使数组的左半部分全部小于x,右半部分全部大于x(这里指的是升序,降序则反过来),中间部分全部等于x。注意,这里左半部分和右半部分长度没必要相等,甚至可以为零。
- 把左半部分当作一个子数组,递归进行第一步操作。
- 把右半部分当作一个子数组,递归进行第一步操作。
- 如果子数组长度为零,则终止操作。
代码如下:
(在交换数组元素位置的时候,有一种非常诡异的方法,那就是利用双指针,直接对数组元素进行交换,不需要额外数组空间,而且只写一个for循环就可以完成数组的交换,花费的时间大概是下面写法的二分之一。但我觉得没啥必要,常数级别的复杂度一般忽略不及,还是采取更容易理解的写法。)
public class Main {
static int[] lxArr = new int[100010]; // 存放小于x的数
static int[] rxArr = new int[100010]; // 存放大于x的数
static void quickSortAsc(int[] arr, int l, int r) {
if(l >= r) {
return;
}
int x = arr[l]; // x随便取一个值
int lxNum = 0; // 小于x的数量
int rxNum = 0; // 大于x的数量
// 把原数组里的元素分成两部分,存放到其他两个数组里
for(int i = l; i <= r; i++) {
if(arr[i] < x) {
lxArr[lxNum++] = arr[i];
}
else if(arr[i] > x) {
rxArr[rxNum++] = arr[i];
}
}
// 把其他两个数组里的元素,重新存放到原数组中
for(int i = l; i <= r; i++) {
if(i < l + lxNum) {
// 把lxArr里的元素存入原数组
arr[i] = lxArr[i - l];
}
else if(i <= r - rxNum) {
//把x存入原数组
arr[i] = x;
}
else {
// 把rxArr里的元素存入原数组
arr[i] = rxArr[rxNum + i - r - 1];
}
}
// 有可能lxNum = 0,这样l就大于r了
quickSortAsc(arr, l, l + lxNum - 1);
quickSortAsc(arr, r - rxNum + 1, r);
}
public static void main(String[] args) {
int[] a = {
10, 4, 1, 9, 100};
quickSortAsc(a, 0, 4);
for (int i = 0; i < a.length; i++) {
System.out.printf("%d ", a[i]);
}
}
}
堆排序
相比于快速排序,堆排序就好写的多了。
但堆排序涉及的知识比较多,首先什么是堆呢,堆就是一颗完全二叉树,该二叉树以及它的所有子树都满足这样一个条件:根节点的值小于等于左右子树所有节点的值,这种二叉树我们称之为小根堆。(反过来,根节点大于等于子树节点,那么就是大根堆。)
如何往一个堆中插入一个元素? 以小根堆为例,直接把该元素插入到二叉树最后一层,比如对于下图中的二叉树,我们就把新元素作为3号节点的右孩子。为了使新的树仍是一个堆,也就是说仍满足上面叙述的条件,我们对插入元素和父元素的值进行比较,如果父元素更大,那么就交换两个元素的值。然后向上递归该操作,如果父元素比子元素大,那么就交换两个元素的值,直至递归到树的顶点。
如何建立一个堆? 首先,只有一个元素的树肯定是一个堆,那么假如要基于n个元素来建立一个堆,我只需要把第一个元素作为初始的堆,然后依次把后续的元素插入到这个堆里即可。
如何从堆顶点取出元素呢? 把顶点元素的值取出,把二叉树的最后一个元素的值赋给顶点元素,然后删除最后一个元素。接下来比较顶点元素和左右孩子的大小,如果不满足堆的条件则顶点元素和较小的孩子进行交换,递归进行该操作,直至二叉树的最后一层。
利用小根堆对数组进行升序排序,代码如下:
import java.util.ArrayList;
// 堆的节点
class HeapNode{
public int value;
public HeapNode(int value) {
this.value = value;
}
}
// 小根堆
class SmallHeap{
// 因为是完全二叉树,所以使用一维数组的形式存储二叉树,根据下标来确定节点之间的父子关系
// 对于下标index,它的父亲是(index - 1) / 2,左儿子是index * 2 + 1,右儿子是index * 2 + 2
public ArrayList<HeapNode> treeNodes = new ArrayList<HeapNode>();
public void addNode(HeapNode node) {
treeNodes.add(node);
pushUp(treeNodes.size() - 1);
}
// 取出树根的元素
public int takeOutRoot() {
int rootValue = treeNodes.get(0).value;
treeNodes.get(0).value = treeNodes.get(treeNodes.size() - 1).value;
treeNodes.remove(treeNodes.size() - 1);
pushDown(0);
return rootValue;
}
public void swap(int index1, int index2) {
int temp = treeNodes.get(index1).value;
treeNodes.get(index1).value = treeNodes.get(index2).value;
treeNodes.get(index2).value = temp;
}
// 把index下标的元素向下递归操作
public void pushDown(int index) {
if(index * 2 + 1 >= treeNodes.size()) {
return ;
}
// 如果只有左孩子没有右孩子
if(index * 2 + 1 == treeNodes.size() - 1) {
if(treeNodes.get(index * 2 + 1).value < treeNodes.get(index).value) {
swap(index, index * 2 + 1);
pushDown(index * 2 + 1);
}
return;
}
// 符合条件,则与左孩子进行交换
if(treeNodes.get(index * 2 + 1).value < treeNodes.get(index).value
&& treeNodes.get(index * 2 + 1).value <= treeNodes.get(index * 2 + 2).value) {
swap(index, index * 2 + 1);
pushDown(index * 2 + 1);
return;
}
// 符合条件,则与右孩子进行交换
if(treeNodes.get(index * 2 + 2).value < treeNodes.get(index).value
&& treeNodes.get(index * 2 + 2).value <= treeNodes.get(index * 2 + 1).value) {
swap(index, index * 2 + 2);
pushDown(index * 2 + 2);
return;
}
}
// 把index下标的元素向上递归操作
public void pushUp(int index) {
if(index <= 0) {
return;
}
// 与父元素值进行交换
if(treeNodes.get(index).value < treeNodes.get((index - 1) / 2).value) {
swap(index, (index - 1) / 2);
pushUp((index - 1) / 2);
}
}
}
public class Main {
static SmallHeap smallHeap = new SmallHeap();
public static void main(String[] args) {
int n = 5;
int[] arr = {
9, 3, 2, 10 , 4};
for (int i = 0; i < n; i++) {
smallHeap.addNode(new HeapNode(arr[i]));
}
for (int i = 0; i < n; i++) {
System.out.print(smallHeap.takeOutRoot() + " ");
}
}
}
并查集
并查集是一种树形的数据结构,可以以接近O(1)的复杂度判断和改变元素所处的集合、合并集合。
核心思想就是,每个集合都对应一棵树,树的根元素代表这个集合的顶点元素。
- 想合并集合时,把一颗树的根元素的加入到另一个树的根元素下面即可
- 想判断两个元素是否位于同一个集合时,判断两个元素所在树的根元素值是否相等即可
- 想改变某个元素所属集合时,把该元素从原树中移除,加入到另一颗树根元素下面即可
并查集的例子不太好举,所以直接找了一道曾经做过的例题,题目如下图所示,代码在题目后面:
class Node{
public Node fatherNode;
public int value;
public Node(int value) {
this.value = value;
}
public void setFatherNode(Node fatherNode) {
this.fatherNode = fatherNode;
}
public Node getRootNode() {
// 如果当前节点没有父节点,那么该节点就是根节点
if(this.fatherNode == null) {
return this;
}
// 如果当前节点有父节点,那么递归调用父节点的函数,并且把父节点更新为根节点,以减少再次查询时的递归次数
this.fatherNode = this.fatherNode.getRootNode();
return this.fatherNode;
}
}
public class Main {
static Node[] nodes = new Node[100010];
static int n, m;
static Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
n = sc.nextInt();
m = sc.nextInt();
for(int i = 1; i <= n; i++) {
nodes[i] = new Node(i);
}
for(int i = 0; i < m; i++) {
String type = sc.next();
int valA = sc.nextInt();
int valB = sc.nextInt();
Node rootOfA = nodes[valA].getRootNode();
Node rootOfB = nodes[valB].getRootNode();
// 注意java判断字符串是否相等不能用 == ,而是要用equals()函数
if(type.equals("M")) {
if(rootOfA != rootOfB) {
// 合并集合
rootOfA.fatherNode = rootOfB;
}
}
else if(type.equals("Q")) {
// 判断是否位于同一个集合
if(rootOfA != rootOfB) {
System.out.println("No");
}
else {
System.out.println("Yes");
}
}
}
}
}