常用数据结构与算法java版01(二分、排序、并查集)

马上各种算法竞赛又要开始了,写这篇博客的主要目的是复习和巩固已经学过的算法,而不是从零开始学习新的算法。
所以对于不会对算法内容进行过多的阐述和讲解,而是以代码展示为主,阅读需要有一定的算法基础。

二分

二分查找(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");
				}
			}
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值