一.Java查找算法之折半查找
相关知识:折半查找又叫作二分查找,是一种在有序数组中查找某一特定元素的搜索算法,使用二分可以极大地缩小我们搜索的时间复杂度
第1关:折半查找(二分查找)
1.任务描述
本关任务:给定一个排好序的数组,然后输入另一个整数,判断该整数在数组中的什么位置,返回该整数第一次出现的位置(位置从0开始),否则返回-1
。
样例 1:
测试输入: n = 6
,nums = [-1,0,3,5,9,12]
, T = 9
预期输出: 4
解释: 9
出现在 nums
中并且下标为 4
样例 2:
测试输入: n = 6
,nums = [-1,0,3,5,9,12]
, T = 2
预期输出: -1
解释: 2
不存在 nums
中因此返回 -1
2.思路分析
最基础的二分查找,以后所有的二分都是在此基础上进行的改进
3.本关答案
package step1;
public class Task {
public int search(int n,int[] nums,int T){
/********* Begin *********/
int l=0;//左指针
int r=n-1;//右指针
//搜索区域: [0,n-1] 搜索结果: 等于T的索引
while(l<=r){
int mid=(l+r)/2;//区间中间比较值
if(nums[mid]>T){//要找的数在区间左边
r=mid-1;
}else if(nums[mid]<T){//要找的数在区间右边
l=mid+1;
}else{//找到了
return mid;
}
}
return -1;
/********* End *********/
}
}
第2关:求平方根
1.任务描述
本关任务:编写一个能求出整数x
平方根的小程序。
使用二分查找,实现int mySqrt(int x)
函数,求平方根的功能。
计算并返回 x
的平方根,其中 x
是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
2.思路分析
简而言之就是找到第一个平方<=target的元素索引
3.本关答案
package step2;
public class Task {
public int mySqrt(int x){
/********* Begin *********/
// 寻找第一个小于等于target的元素
int l=0;
int r=x;
int ans=-1;
//搜索区间: [1,x] 搜索结果: 第一个平方<=target的元素索引
while(l<=r){
int mid = (l+r)/2;
if((long) mid * mid <= x){
l=mid+1;
ans=mid;
}else{
r=mid-1;
}
}
return ans;
/********* End *********/
}
}
第3关:搜索数组
1.任务描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组[0,1,2,4,5,6,7]
可能变为[4,5,6,7,0,1,2]
)。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它在数组中的下标,否则返回 -1
。
已知的是,数组中不存在重复的元素。
你的算法时间复杂度必须是 O(logn) 级别。
2.思路分析
二分查找的前提是有序!!!但这题是在原来有序的数组基础上进行了反转,最后我们可以看作是两个有序区间:右区间(大数)以及左区间(小数)
因此本题我们的思路就是在两个有序的区间中找到答案,因此我的重点就是如何在两个区间中来回寻找
首先我们要判断mid在哪个区间中,若是在右区间中,我们就在右区间进行区间缩小;若是在左区间中,我们就在左区间进行区间缩小。
当mid在右区间时,我们要考虑两种情况:什么条件才能区间左移或右移
这里当T也在右区间且小于T,向左缩小查找区间
这里无论T在不在右区间,我们都要右移
mid在左区间时同理
这样就是在两个有序区间中进行二分的逻辑
3.本关答案
package step3;
public class Task {
public int search(int n,int[] nums,int T){
/********* Begin *********/
if (n == 1) {
return nums[0] == T ? 0 : -1;
}
//搜索区间: [右半段] & [左半段] 搜索结果: 等于target的索引
int l = 0, r = n - 1;
while (l <= r) {
int mid = (l + r) / 2;
if (nums[mid] == T) {//找到了
return mid;
}
if (nums[0] <= nums[mid]) {//mid在右半段区间中
if (nums[0] <= T && T < nums[mid]) {//T也在右区间,向左缩小查找区间
r = mid - 1;
} else {//向右缩小查找区间
l = mid + 1;
}
} else {//mid在右半段区间中
if (nums[mid] < T && T <= nums[n - 1]) {//T也在左区间,向右缩小查找区间
l = mid + 1;
} else {//向左缩小查找区间
r = mid - 1;
}
}
}
return -1;
/********* End *********/
}
}
二.Java 数据结构之排序
第1关:选择排序
1.任务描述
本关任务:实现选择排序。
- 补全
void sort(int arr[])
方法,实现选择排序,对数组arr
中的元素排序,并输出每一次排序后的结果。
2.思路分析
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕
其核心思想是在每一个索引位置,寻找其后面元素组的最小(大)元素交换
3.本关答案
package step1;
/**
* Created by sykus on 2018/3/20.
*/
public class SelectionSort {
/**
* 选择排序
*
* @param arr
*/
public static void sort(int arr[]) {
/********** Begin *********/
int n=arr.length;
for(int i=0;i<n-1;i++){//待排列元素
for(int j=i+1;j<n;j++){//未排列元素
if(arr[j]<arr[i]){
int tem=arr[i];
arr[i]=arr[j];
arr[j]=tem;
}
}
print(arr);
}
/********** End *********/
}
private static void print(int arr[]) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
第2关:插入排序
1.任务描述
本关任务:实现插入排序。
- 补全
sort(int arr[])
方法,实现插入排序功能,并且输出每一次排序结果。
2.思路分析
插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数组。
我们可以将待排序的数组分为两部分:有序数组和无序数组;那么插入排序就是将无序数组中的元素,插入到有序数组中
3.本关答案
package step2;
/**
* Created by sykus on 2018/3/20.
*/
public class InsertionSort {
public static void sort(int arr[]) {
/********** Begin *********/
for(int i=1;i<arr.length;i++){//遍历无序数组,对每一个元素进行排序
int j=i;
int tem=arr[j];
//与有序数组的元素比较,找到合适的位置
while(j>0 && tem<arr[j-1]){
arr[j]=arr[j-1];
j--;
}
arr[j]=tem;
print(arr);
}
/********** End *********/
}
private static void print(int arr[]) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
第3关:归并排序
1.任务描述
本关任务:实现归并排序。
- 补全
merge(int arr[], int p, int q, int r)
方法,对数组arr
中的两个序列arr[p..q]
和arr[p+1..r]
实现归并操作。
2.思路分析
归并是指将若干个已排序的子序列合并成一个有序的序列。
归并排序的核心就是先分组再排序再合并;我们先只看一路递归,对于未排序的数组,我们要将其分为两组子序列,再将这两组子序列继续递归排序成为有序的两组子序列,最后我们再将这两组子序列合并,就得到了一个排序好的数组了
3.本关答案
package step3;
/**
* Created by sykus on 2018/3/20.
*/
public class MergeSort {
/**
* lo, hi都是指下标
*/
public static void sort(int arr[], int lo, int hi) {
if (lo < hi) {
int mid = (lo + hi) / 2;//开始分组
//对两组子序列递归排序
sort(arr, lo, mid);
sort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);//合并排序
print(arr);
}
}
private static void merge(int arr[], int p, int q, int r) {
/********** Begin *********/
//临时数组,用来记录合并后的数组
int[] tem=new int[arr.length];
//要合并的两个数组的索引下标
int i=p;
int j=q+1;
//记录临时数组tem的索引
int k=p;
//排序,填充临时数组
while(i<=q && j<=r){
if(arr[i]<arr[j]){
tem[k++]=arr[i++];
}else{
tem[k++]=arr[j++];
}
}
//加入剩余的数组
while(i<=q){
tem[k++]=arr[i++];
}
while(j<=r){
tem[k++]=arr[j++];
}
//将临时数组拷贝给arr
for(int a=p;a<=r;a++){
arr[a]=tem[a];
}
/********** End *********/
}
private static void print(int arr[]) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
第4关:快速排序
1.任务描述
本关任务:实现快速排序。
- 补全方法
sort(int arr[], int low, int high)
,实现快速排序功能。low
,high
为数组下标。 - 程序输出每次交换的结果。
2.思路分析
快速排序就是:通过一趟排序将要排序的数据分割成两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
因此对于每轮递归,我们要按其基准数排序将数组分为两个有序序列,再对这两个子序列进行递归重复操作
public void sort(int arr[], int low, int high) {
//记录查找的范围
int i=low;
int j=high;
//递归出口
if(low>high){
return;
}
//基准数
int baseNum=arr[low];
while(i!=j){
//找比基准数小的数
while(true){
if(j<=i || arr[j]<baseNum){
break;
}
j--;
}
//找比基准数大的数
while(true){
if(j<=i || arr[i]>baseNum){
break;
}
i++;
}
if(i>=j) break;
//交换数据,使比基准数小的在左边,大的在右边
int tem=arr[i];
arr[i]=arr[j];
arr[j]=tem;
}
//基准数归位
int tem=arr[i];
arr[i]=arr[low];
arr[low]=tem;
//递归进行下一轮基准数排序
sort(arr,low,i-1);
sort(arr,i+1,high);
}
3.本关答案
package step4;
/**
* Created by sykus on 2018/3/20.
*/
public class QuickSort {
public void sort(int arr[], int low, int high) {
/********** Begin *********/
int i=low;
int j=high+1;
int povit=arr[low];
while(i<j){
while(j>low && arr[--j]>=povit);
while(i<high && arr[++i]<=povit);
if(i>=j) break;
int tem=arr[j];
arr[j]=arr[i];
arr[i]=tem;
print(arr);
}
int tem=arr[j];
arr[j]=arr[low];
arr[low]=tem;
print(arr);
if(i>low) sort(arr,low,j-1);
if(j<high)sort(arr,j+1,high);
/********** End *********/
}
private static void print(int arr[]) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
第5关:堆排序
1.任务描述
本关任务:在本关,我们将实现另一种排序算法——堆排序(heapsort
)。
-
平台将调用
HeapSort
类的sort(int arr[])
方法,对arr
进行排序,并输出每次堆排序的结果 -
接着根据程序的输出判断程序是否正确。
2.思路分析
堆排序(Heapsort
)是指利用堆这种数据结构所设计的一种排序算法,其中堆分为大顶堆和小顶堆。
堆的定义:当一颗二叉树的每个节点都大于等于它的两个子字节时,它被称为堆有序。而堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级储存
堆的性质:在索引起始位置为0的情形中: 下标为i的节点的父节点下标:(i-1)/2;下标为i的节点的左孩子下标:i*2+1;下标为i的节点的右孩子下标:i*2+2
堆排序(大顶堆):首先我们要先建堆,将未排序的数组进行建堆处理使其成为一个大顶堆
弗洛伊德建堆算法思想:找到最后一个非叶子节点,从后向前依次下潜(维护该节点树的堆的性质),下潜就是该节点与其左右两个孩子的最大者进行交换,重复此操作,直到叶子结点。
当我们建堆完成后就得到了一个大顶堆,而此时arr[0]即为堆顶元素
然后我们再来进行堆排序,其思想就是将堆顶元素与数组末尾元素交换,然后,去除交换后的排序数组,我们再去维护交换后的堆使其再次成为一个大顶堆,重复操作
3.本关答案
package step5;
/**
* Created by sykus on 2018/3/20.
*/
public class HeapSort {
public static void sort(int arr[]) {
/********** Begin *********/
int n=arr.length;
//建堆(弗洛伊德建堆算法)
//找到最后一个非叶子节点(n/2-1),从后向前依次下潜,维护堆的性质
for(int i=n/2-1;i>=0;i--){
down(arr,i,n);
}
//堆排序
//堆顶元素后移,维护堆
for(int i=n-1;i>0;i--){
int tem=arr[0];
arr[0]=arr[i];
arr[i]=tem;
down(arr,0,i);
print(arr);
}
/********** End *********/
}
//下潜维护堆的性质
//parent:待维护节点的下标 n:维护的元素范围[0,n]
public static void down(int[] arr,int parent,int n){
//先假设父节点最大,比较得到三者的最大节点下标
int max=parent;
int leftchild=2*parent+1;
int rightchild=2*parent+2;
if(leftchild<n && arr[leftchild]>arr[max]){
max=leftchild;
}
if(rightchild<n && arr[rightchild]>arr[max]){
max=rightchild;
}
//若假设的父节点不是最大,则交换继续向下下潜维护堆
if(max!=parent){
int tem=arr[parent];
arr[parent]=arr[max];
arr[max]=tem;
parent=max;
down(arr,parent,n);
}
}
private static void print(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
}
三.Java 数据结构之图
第1关:图的表示
1.任务描述
本关任务:学习图的相关概念和表示,并用邻接表示图。
-
平台将创建用户补全后的
Graph
类的对象。 -
调用对象的
addEdge(int v, int w)
方法,添加边构造一个图。 -
接着根据程序的输出判断程序是否正确。
2.相关知识
图由顶点(Vertex
)和边(Edge
)组成,顶点的集合是V
、边的集合是E
的图记为G=(V, E)
,连接两点u
和v
的边用e=(u, v)
表示
图的种类:图大体上分为2
种。边没有指向性的图叫做无向图,边具有指向性的图叫做有向图。边上带有权值的图叫带权图
图的表示:邻接矩阵:使用大小为|V|×|V|
的二维数组G
来表示图。G[i][j]
表示的是顶点i
和顶点j
的关系;邻接表:用一点到另一点的边在链表中来表示图
3.思路分析
本关要求我们完成图的邻接矩阵表示,首先我们来分析代码中已经给出的成员变量以及方法
其中给出了3个成员变量:V,E和一个邻接表;一个有参构造:是对于成员变量的初始化
我们需要了解的是给出的邻接表表示的是:每个顶点与之有关系的点的集合的数组
private int V;//顶点数
private int E;//边数
private ArrayList<Integer>[] adj;//邻接表,集合数组
public Graph(int v) {
if (v < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative");
V = v;
E = 0;
//index+1=v 索引+1=顶点
//如:1顶点在0索引处
adj = new ArrayList[V + 1];
for (int i = 0; i <= this.V; i++) {
adj[i] = new ArrayList<Integer>();
}
}
本关的任务简言之就是写出一个添加两点之间的边的方法,那么我们就直接在邻接表中赋值即可
4.本关答案
package step1;
import java.util.ArrayList;
public class Graph {
private int V;//顶点数
private int E;//边数
private ArrayList<Integer>[] adj;//邻接表,集合数组
public Graph(int v) {
if (v < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative");
V = v;
E = 0;
adj = new ArrayList[V + 1];
for (int i = 0; i <= this.V; i++) {
adj[i] = new ArrayList<Integer>();
}
}
public void addEdge(int v, int w) {
/********** Begin *********/
adj[v].add(w);
adj[w].add(v);
E++;
/********** End *********/
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append(V + " 个顶点, " + E + " 条边\n");
for (int v = 1; v <= V; v++) {
s.append(v + ": ");
for (int w : adj[v]) {
s.append(w + " ");
}
s.append("\n");
}
return s.toString();
}
}
第2关:深度优先搜索
1.任务描述
本关任务:实现深度优先搜索。
-
平台将创建用户补全后的
DFSGraph
类的对象; -
调用对象的
void addEdge(int v, int w)
方法,构造一个图; -
调用对象的
void DFS(int v)
方法,从顶点v
开始进行深度优先搜索,测试文件中是从1
号顶点开始; -
接着根据程序的输出判断程序是否正确
2.相关知识
图的深度优先搜索(Depth-First Search,DFS
),顾名思义就是深度遍历节点,也就是依次向下遍历节点的关系节点,直到该节点没有下一个关系节点或该节点已经遍历过了,我们再返回遍历最先前节点的其余节点
如下图所示:先遍历的A,然后遍历A的关系节点C,递归,再遍历C的关系节点B,B无关系节点,回溯到C,再遍历C的关系节点D,D的关系节点A遍历过了,回溯到C,C遍历完成,回溯到A,A的关系节点D,D遍历过了跳过,再遍历A的关系节点F,一直向下递归遍历,直到所有节点都遍历完成
3.思路分析
深度优先搜索是一个递归的过程,我们需要依次向下遍历节点的关系节点,直到该节点没有下一个关系节点或该节点已经遍历过了,我们再返回遍历最先前节点的其余节点
因此我们需要一个boolean数组用来记录节点是否遍历过
4.本关答案
package step2;
import java.util.ArrayList;
public class DFSGraph {
private boolean[] marked;
private int V;//顶点数
private int E;//边数
private ArrayList<Integer>[] adj;//邻接表
public DFSGraph(int v) {
if (v < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative");
V = v;
E = 0;
adj = new ArrayList[V + 1];
marked = new boolean[V + 1];
for (int i = 0; i <= this.V; i++) {
adj[i] = new ArrayList<Integer>();
}
}
public void addEdge(int v, int w) {
adj[v].add(w);
adj[w].add(v);
E++;
}
public void DFS(int v) {
/********** Begin *********/
if(marked[v]){//该节点遍历过了,回溯
return;
}
//遍历该节点
marked[v] = true;
System.out.print(v + " ");
//遍历与该节点有关系且未遍历的节点
for (int w : adj[v]) {
if (!marked[w]) {
DFS(w);//递归
}
}
/********** End *********/
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append(V + " 个顶点, " + E + " 条边\n");
for (int v = 1; v <= V; v++) {
s.append(v + ": ");
for (int w : adj[v]) {
s.append(w + " ");
}
s.append("\n");
}
return s.toString();
}
}
第3关:广度优先搜索
1.任务描述
本关任务:实现图的广度优先搜索
-
平台将创建用户补全后的
BFSGraph
类的对象; -
调用对象的
void addEdge(int v, int w)
方法,构建一个图; -
调用对象的
void BFS(int s)
方法从结点s
开始广度优先搜素; -
接着根据程序的输出判断程序是否正确。
2.相关知识
广度优先搜索算法(Breadth First Search
),又称为"宽度优先搜索"或"横向优先搜索",简称BFS,也就是先遍历一个节点的所有节点,再向下遍历它的关系节点中未遍历的节点
3.思路分析
我们要先遍历该节点的所有关系节点,再去遍历其它的未遍历节点,那么我们就要在遍历该节点的时候用一个队列记录它的关系节点,队列先进先出的性质,这样我们就可以完成先遍历关系节点,再去遍历关系节点的关系节点的广度优先了
4.本关答案
package step3;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class BFSGraph {
private int V;//顶点数
private int E;//边数
private boolean[] marked;
private ArrayList<Integer>[] adj;//邻接表
public BFSGraph(int v) {
if (v < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative");
V = v;
E = 0;
adj = new ArrayList[V + 1];
marked = new boolean[V + 1];
for (int i = 0; i <= this.V; i++) {
adj[i] = new ArrayList<Integer>();
}
}
public void addEdge(int v, int w) {
adj[v].add(w);
adj[w].add(v);
E++;
}
public void BFS(int s) {
/********** Begin *********/
//从s节点开始遍历
Queue<Integer> que = new LinkedList<>();
que.offer(s);
marked[s] = true;
while (!que.isEmpty()) {
//遍历该节点
int v = que.poll();
System.out.print(v + " ");
//将该节点的未遍历的关系节点入队
for (int w : adj[v]) {
if (!marked[w]) {
que.offer(w);
marked[w] = true;
}
}
}
/********** End *********/
}
public String toString() {
StringBuilder s = new StringBuilder();
s.append(V + " 个顶点, " + E + " 条边\n");
for (int v = 1; v <= V; v++) {
s.append(v + ": ");
for (int w : adj[v]) {
s.append(w + " ");
}
s.append("\n");
}
return s.toString();
}
}
第4关:单源最短路径
1.任务描述
本关任务:实现Dijkstra
算法求单源最短路径
-
平台将创建用户补全后的
ShortestPath
类的对象; -
调用对象的
addEdge(int u, int v, int w)
方法,添加边和边的权重信息,构建图G
; -
调用对象的
Paths(int source)
方法执行Dijkstra
算法,求最短路径,并输出返回的最短路径,这里源点设置为1
; -
接着根据程序的输出判断程序是否正确
2.相关知识
单源最短路径:从某一个点开始,到其他所有点的最短路径
迪杰斯特拉算法(Dijkstra算法):是一个按路径长度递增的次序产生最短路径的算法,要求所有边的权重都为非负值
运用贪心的思想,局部:对于每一次的路径,我们首先要选择未走过路径的最小权值路径min,对于min的每一个关系节点,若min的权值到该节点的权值小于该节点的先前节点,就将更换该节点的权值;由局部推出整体为最优
如下图所演示:由A到其他所有点的最短路径
首先我们要先将路径初始化,源点到源点为0,其余先设为无穷大
然后选取权值最小点A,更新由它到关系节点的最小权值,更新完成后去除该点
再选取最小权值点D,并更新去除
接着重复操作
最后遍历完成后所取得的各点权值即为源点到各点的最短路径
3.思路分析
我们根据迪杰斯特拉算法的思想,可以获取到以下的步骤:首先获取最小权值点,再更新权值,再删除节点,重复操作直到所有节点都走过
获取最小权值点,我们可以利用集合实现,每次遍历集合比较获取;也可以利用优先队列实现,每次获取堆顶元素
4.本关答案
package step4;
import java.util.*;
public class ShortestPath {
private int V;//顶点数
private int E;//边数
private int[] dist;
private ArrayList<Integer>[] adj;//邻接表
private int[][] weight;//权重
public ShortestPath(int v, int e) {
V = v;
E = e;
dist = new int[V + 1];
adj = new ArrayList[V + 1];
weight = new int[V + 1][V + 1];
for (int i = 0; i <= this.V; i++) {
adj[i] = new ArrayList<Integer>();
}
}
public void addEdge(int u, int v, int w) {
adj[u].add(v);
adj[v].add(u);
weight[u][v] = weight[v][u] = w;
}
public int[] Paths(int source) {
/********** Begin *********/
ArrayList<Integer> list = new ArrayList<>();
boolean[] flag=new boolean[V+1];
// 源点到源点的距离为0
dist[source] = 0;
// 初始化
for (int i = 1; i <= V; i++) {
// 从源点到各个节点的距离初始化为无穷大
if (i != source) {
dist[i] = Integer.MAX_VALUE;
}
//index+1=v
list.add(i);
}
while (!list.isEmpty()) {
//获取权值最小点
int min=list.get(0);
for(int i=0;i<list.size();i++){
min=dist[min]<dist[list.get(i)]?min:list.get(i);
}
flag[min]=true;
//更新权值
for (int e : adj[min]) {
if(!flag[e]){
int alt = dist[min] + weight[min][e];
// 找到了到u的更短的路径
if (alt < dist[e]) {
dist[e] = alt;
}
}
}
//删除节点
list.remove((Integer)min);
}
return dist;
/********** End *********/
}
/**
* 打印源点到所有顶点的距离,INF为无穷大
*
* @param dist
*/
public void print(int[] dist) {
for (int i = 1; i <= V; i++) {
if (dist[i] == Integer.MAX_VALUE) {
System.out.print("INF ");
} else {
System.out.print(dist[i] + " ");
}
}
}
}