二分搜索
时间复杂度:O(logn)
二分搜索技术(从排好序的n个元素(由小到大排序)中寻找一个中寻找元素x的坐标)
输入举例(即给十个数,找里面的31的坐标):
10 321
1 1 2 3 5 7 12 31 111 321
输出举例
7
package section2.BinarySearch;
import java.util.Scanner;
public class BinarySearch {
public static void main(String[] args) {
Scanner cin =new Scanner(System.in);
int total = cin.nextInt(); //数据个数
int num = cin.nextInt(); //要找的数据
int[] array = new int[total];
for (int i = 0; i < array.length; i++) {
array[i]=cin.nextInt();
}
System.out.println(find(total,num,array));
}
public static int find(int total, int num, int[] arr){
int left = 0; int right =total-1;
while(left<=right){
int middle = (left+right)/2; //每次查找都缩小一半范围
if(arr[middle]==num){ //找到
return middle;
}else if(arr[middle]>num){
right = middle-1; //注意-1 避免(8+9)/=8这样的死循环
}else{
left=middle+1; //注意+1 避免(8+9)/=8这样的死循环
}
}
return -1; //没找到
}
}
大整数乘法
请设计一个有效的算法,可以进行两个n位大整数的乘法运算
1 最暴力的方法:O(n^2)
2 我们采用分而治之的思想
将X和Y按如下方法分成两部分
那么
X = A10^(n/2) + B
Y = C10^(n/2) + D
X*Y = (A*10^(n/2) + B)*( C*10^(n/2) + D)
= A*C*10^n + A*D*10^(n/2) + B*C*10^(n/2) + B*D
复杂度分析
为了降低时间复杂度,必须减少乘法的次数上式可改写为
X*Y = (A*10^(n/2) + B)*( C*10^(n/2) + D)
= A*C*10^n + A*D*10^(n/2) + B*C*10^(n/2) + B*D
= A*C*10^n + ((A+B)(C+D) – A*C – B*D)*10*(n/2) + B*D
或者
= A*C*10^n + ((A-B)(D-C) + A*C + B*D)*10*(n/2) + B*D
复杂度分析
package section2.Divide_and_Conquer;
public class BigDataMultiple {
public static void main(String[] args) {
// 大整数相乘
long x = 12234L;
long y = 45243L;
String sx = String.valueOf(x); //sx为12234
int n = sx.length();
long sig = sign(x) * sign(y); //符号运算
double s = bigdataride(x, y, n);
System.out.println("大数相乘的计算结果为:" + s * sig);
}
public static int sign(long a) {
return a < 0 ? -1 : 1;
}
public static double bigdataride(long x, long y, int n) {
x = Math.abs(x);
y = Math.abs(y);
if (n == 1) {
return x * y;
}else{
if (n % 2 == 1) {
n = n - 1; // 若数字长度n为奇数,则转化为偶数
}
long a = x / Math.round(Math.pow(10, (n / 2))); //x的前半段
long b = x - a * Math.round(Math.pow(10, (n / 2))); //x的后半段
long c = y / Math.round(Math.pow(10, (n / 2))); //y的前半段
long d = y - c * Math.round(Math.pow(10, (n / 2))); //y的后半段
double ac = bigdataride(a, c, n / 2);// 递归计算a*c(每一次都拆分为两半)
double bd = bigdataride(b, d, n / 2);// 计算b*d
long aJb = a + b;
long cJd = c + d;
double abcd = bigdataride(aJb, cJd, n / 2);
return (ac * Math.pow(10, n) + (abcd - ac - bd)* Math.pow(10, n / 2) + bd); //每一轮递归都产生一个拆分好几次后得到的两个数的乘积
}
}
}
Strassen矩阵乘法
暴力的矩阵乘法时间复杂度为O(n的三次方)
Strassen矩阵乘法采用的方法与大整数乘法方法类似,利用分治的策略,将一个矩阵递归的划分为一半,只不过大整数乘法是只在数的长度上分为了两半,而Strassen矩阵乘法是将矩阵在长和宽上都划分为了两半,即第一次时将一个矩阵分为四个矩阵
时间复杂度变为
由于和大整数乘法一样改进不是很大,所以以后有时间在学,先写出暴力法矩阵乘法的代码
//暴力法矩阵乘法
package section2.Divide_and_Conquer;
public class Strassen {
public static void main(String[] args) {
int[][] x1 = {{1,2,3,4,5},{5,4,3,2,1},{5,4,3,2,1}}; //第一个矩阵的行数要和第二个矩阵的列数相等
int[][] x2 = {{3,2},{1,2},{5,3},{1,2},{7,3}}; //第二个矩阵的行数要和第一个矩阵的列数相等
int[][] result = new int[x1.length][x2[0].length]; //结果矩阵的行数等于x1的行数,结果的列数等于x2的列数
for (int i = 0; i < x1.length; i++) { //结果数组的第i行
for (int j = 0; j < x2[0].length; j++) { //结果数组的第j列
int num = 0; //记录result[i][j]的值
for (int k = 0; k < x1[0].length; k++) { //结果中的每个数都需要经历k次运算
num = num + x1[i][k]*x2[k][j];
}
result[i][j] = num;
}
}
for (int i = 0; i < result.length; i++) { //输出结果数组
for (int j = 0; j < result[0].length; j++) {
System.out.print(result[i][j]+" ");
}
System.out.println();
}
}
}
棋盘覆盖
//棋盘覆盖
//问题:用4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
//思路:应用分治法
//分治的技巧在于如何划分棋盘,使划分后的子棋盘的大小相同,并且每个子棋盘均包含一个特殊方格,从而将原问题分解为规模较小的棋盘覆盖问题。
//k>0 时,可将2^k×2^k的棋盘划分为4个2^(k-1)×2^(k-1)的子棋盘,如图4.11(a)所示。这样划分后,由于原棋盘只有一个特殊方格,所 以,
//这4个子棋盘中只有一个子棋盘包含该特殊方格,其余3个子棋盘中没有特殊方格。为了将这3个没有特殊方格的子棋盘转化为特殊棋盘,以便采用递归方法求 解,
//可以用一个L型骨牌覆盖这3个较小棋盘的会合处,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种划分策略, 直至将棋盘分割为1×1的子棋盘。
package section2.Divide_and_Conquer;
import java.util.Scanner;
public class ChessboardCoverage {
public static void main(String[] args) {
int size = 4; // 棋盘的长,固定为4
int dr = 1; // 特殊棋子所在的行
int dc = 1; // 特殊棋子所在的列
ChessboardCoverage c = new ChessboardCoverage();
c.chessBoard(0, 0, dr, dc, size);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
int title = 1; // 表示L型骨牌的编号
static int[][] board = new int[4][4];
// 处理带有特殊棋子的棋盘.tr、tc表示棋盘的入口即左上角的行列号,dr、dc表示特殊棋子的行列位置,size表示棋盘的行数或者列数
public void chessBoard(int tr, int tc, int dr, int dc, int size) {
if (size == 1) return;
int t = title++; // 骨牌编号
System.out.println(t);
int s = size / 2;// 每一次化大棋盘为一半的子棋盘
// 要处理带有特殊棋子的棋盘,第一步先处理左上棋盘(后面三步与此步重复)
if (dr < tr + s && dc < tc + s)// 左上角子棋盘有特殊棋子
chessBoard(tr, tc, dr, dc, s);// 处理有特殊棋子的左上角子棋盘
else// 处理无特殊棋子的左上角子棋盘
{
board[tr + s - 1][tc + s - 1] = t;// 设左上角子棋盘的右下角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
chessBoard(tr, tc, tr + s - 1, tc + s - 1, s);// 处理有用骨牌覆盖的格子作为特殊棋子的左上角子棋盘
}
// 第二步处理右上角棋盘
if (dr < tr + s && dc >= tc + s)// 右上角子棋盘有特殊棋子
{
chessBoard(tr, tc + s, dr, dc, s);// 处理有特殊棋子的右上角子棋盘
} else {
board[tr + s - 1][tc + s] = t;// 设右上角子棋盘的左下角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
chessBoard(tr, tc + s, tr + s - 1, tc + s, s);// 处理有用骨牌覆盖的格子作为特殊棋子的右上角子棋盘
}
// 第三步处理左下角子棋盘
if (dr >= tr + s && dc < tc + s)// 左下角子棋盘有特殊棋子
{
chessBoard(tr + s, tc, dr, dc, s);// 处理有特殊棋子的左下角子棋盘
} else {
board[tr + s][tc + s - 1] = t;// 设左下角子棋盘的右上角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
chessBoard(tr + s, tc, tr + s, tc + s - 1, s);// 处理有用骨牌覆盖的格子作为特殊棋子的左下角子棋盘
}
// 第四步处理右下角棋盘
if (dr >= tr + s && dc >= tc + s)// 右下角子棋盘有特殊棋子
{
chessBoard(tr + s, tc + s, dr, dc, s);// 处理有特殊棋子的右下角子棋盘
} else {
board[tr + s][tc + s] = t;// 设子棋盘右下角的左上角为特殊棋子,用t型的骨牌覆盖。由于骨牌有三种,当处理过程中同一级设置的特殊棋子用相同的骨牌覆盖
chessBoard(tr + s, tc + s, tr + s, tc + s, s);// 处理有用骨牌覆盖的格子作为特殊棋子的右下角子棋盘
}
}
}
合并排序
时间复杂度:nlog2n
如果a的x次方=N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN
O(lgn):
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推
O(nlgn):
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推,在此过程中同时遍历每一半数据
//合并排序
package section2.Divide_and_Conquer;
public class MergeSort {
public static void main(String[] args) {
int[] arr = {1,2,9,8,3,7,6,5,4,3,2,1};
sort(0,11,arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
public static void sort(int begin,int end,int[] arr){ //往里递归时每次1/2的分割数组,往外递归的时候合并排序
if(begin<end){
sort(begin,(begin+end)/2,arr); //左边归并排序,使得左子序列有序
sort((begin+end)/2+1,end,arr); //右边归并排序,使得右子序列有序
merge(begin,end,arr); //将两个有序子数组合并操作(其实真正的排序都在这里面)
}
}
public static void merge(int begin,int end,int[] arr){
int i = begin; //左序列指针
int j = (begin+end)/2+1; //右序列指针
int[] temp = new int[arr.length]; //临时数组
int index = 0; //临时数组指针
while(i<=(begin+end)/2&&j<=end){ //此while排完之后左右数组中交叉大小的已经合并完毕
//剩下的是较大的部分,不用排序了,直接接在后面,在后面的两个while中实现
if(arr[i]>arr[j]){
temp[index] = arr[j];
index++;
j++;
}else{
temp[index] = arr[i];
index++;
i++;
}
}
while(i<=(begin+end)/2){ //将左边剩余元素填充进temp中
temp[index] = arr[i];
index++;
i++;
}
while(j<=end){ //将右序列剩余元素填充进temp中
temp[index] = arr[j];
index++;
j++;
}
//将temp中的元素全部拷贝到原数组中
for (int k = begin; k <=end ; k++) {
arr[k] = temp[k-begin];
}
}
}
快速排序
时间复杂度:nlog2n
//快速排序(极为重要)
package section2.Divide_and_Conquer;
public class QuickSort {
public static void main(String[] args) {
int[] arr = {1, 49, 38, 65, 97, 23, 22, 76, 1, 5, 8, 2, 0, -1, 22 };
quickSort(arr, 0, arr.length - 1);
System.out.println("排序后:");
for (int i : arr) {
System.out.print(i+" ");
}
}
private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找寻基准数据的正确索引
int index = getIndex(arr, low, high); //每次运行之后arr都会改变
// 进行迭代对index之前和之后的数组进行相同的操作使整个数组变成有序
//(因为index之前的数在一次quickSort之后已经都比index处的值小,且index之后都比index大
//所以现在分别对index前的数在排序,对index后的数在排序,index处的数不用变了)
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
private static int getIndex(int[] arr, int low, int high) {
// 基准数据
int tmp = arr[low];
while (low < high) {
// 当队尾的元素大于等于基准数据时,向前挪动high指针
while (low < high && arr[high] >= tmp) {
high--;
}
// 如果队尾元素小于tmp了,需要将其赋值给low
arr[low] = arr[high];
// 当队首元素小于等于tmp时,向前挪动low指针
while (low < high && arr[low] <= tmp) {
low++;
}
// 当队首元素大于tmp时,需要将其赋值给high
arr[high] = arr[low];
}
// 跳出循环时low和high相等,此时的low或high就是tmp的正确索引位置
// 由原理部分可以很清楚的知道low位置的值并不是tmp,所以需要将tmp赋值给arr[low]
arr[low] = tmp;
return low; // 返回tmp的正确位置
}
}
循环赛日程表
问题描述
设有n=2k个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
每个选手必须与其他n-1个选手各赛一次;
每个选手一天只能参赛一次;
循环赛在n-1天内结束。
请按此要求将比赛日程表设计成有n行和n-1列的一个表。在表中的第i行,第j列处填入第i个选手在第j天所遇到的选手。其中1≤i≤n,1≤j≤n-1。
package section2.Divide_and_Conquer;
import java.util.Scanner;
public class RoundRobinSchedule {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int k = in.nextInt();
int n = 1; //参赛人数为2的k次方
for (int i = 0; i < k; i++) {
n = n*2;
}
int[][] result = table(n,k);
for (int i = 0; i < result.length; i++) {
for (int j = 0; j < result.length; j++) {
System.out.print(result[i][j]+" ");
}
System.out.println();
}
}
public static int[][] table(int n,int k){
int[][] table = new int[n][n];
for (int i = 0; i < table.length; i++) {
table[0][i] = i+1;
}
int m = 1;
for (int i = 1; i <= k; i++) { //可以等分k次,即分治k次,k:参赛人数为2的k次方
m = m*2; //此次处理的正方形长
for (int z = 0; z < n/m; z++) { //每一轮的前m行,n列都要进行相似处理,m在变,n不变(即处理多少个正方形)
for (int j = 0; j < m/2; j++) { //x 处理的其中一个正方形的x坐标
for (int x = 0; x < m/2; x++) { //y 处理的其中一个正方形的y坐标
table[j+m/2][x+m*z] = table[j][x+m/2+m*z]; //正方形的左下角等于右上角
}
for (int s = m/2; s < m; s++) { //y 处理的其中一个正方形的y坐标
table[j+m/2][s+m*z] = table[j][s-m/2+m*z]; //正方形的右下角等于左上角
}
}
}
}
return table;
}
}