排序
知识点
在一维数组中,排序是很重要的。
常见的排序分为:
- 冒泡排序。
- 选择排序。
- 插入排序。
冒泡排序
原理
比较两个相邻元素的值,将较大值的元素交换到右边。
选择排序
原理
记录第一个元素,然后逐个与后面的元素进行比较,然后记录最大的(最小的)元素,然后与第一个元素进行交换位置。
然后再记录第二个元素,与后面的元素再进行比较,然后与第二个元素交换位置。
依次类推
插入排序
原理
- 将数列中的数字分为有序和无序两部分,一个数字是有序的。
- 所以将整个数列分出来一个数字,它是有序的部分。另外的都是无序的部分,然后在无序部分中的数字一一取出与有序部分进行比较。
- 数字先与前一个数字进行比较,如果没有比较出结果,则继续向前进行比较,直到找到合适的位置。(期间需要不断判断)
即:找到合适的位置进行插入
三种排序的比较与理解
- 冒泡排序:相连两个进行比较,这样最大的就依次向后推了。
- 选择排序:选出最大的放在第一位,然后第二大的放在第二位,依次。
- 插入排序:每一个数字选择合适的位置进行插入。
例题(排序)
例题1(冒泡排序)
对【10,1,35,61,89,36,55】进行排序
思路:
- 使用冒泡排序
- 双层循环 分为:每一趟和每一次
- 优化:每一趟跑完,最大的一个元素就排到了倒数第 i 个位置。故可以不用比较倒数第 i 个元素和倒数第 i-1 个元素
代码如下:
package test01;
//冒泡排序
public class Main_P9 {
public static void main(String args[]) {
int [] a = {10,1,35,61,89,36,55};
for(int i = a.length; i>1;i--) {
for(int n = 0;n<i-1;n++) {
int flag = 0;
if(a[n]>a[n+1]) {
//如果左边的数大于右边的数,则把这个数放到右边
flag = a[n];
a[n] = a[n+1];
a[n+1] = flag;
}
}
}
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
例题2(比较排序)
对【2,9,5,0,1,3,6,8】进行从小到大的排序。
思路分析:
- 使用两个变量,一个保存最小值,一个记录下标
- 然后嵌套循环,外层来决定从哪个元素开始,内层来逐个与后面的元素进行比较
- 到内层循环碰到较小的值,则记录这个值和角标
- 然后再一次外层循环结束之后将较小值元素与当道当前循环次数位置的元素交换(如:将第一小的元素与第一位元素进行交换;将第二小的元素与第二位元素进行交换;依次类推)
代码如下:
package test01;
public class Main_P10 {
public static void main(String[] args) {
int [] a = {2,9,5,0,1,3,6,8};
int flag; //交换值
int num = 0; //角标
for (int i = 0; i < a.length; i++) {
flag = a[i];
for (int j = i+1; j < a.length-1; j++) {
//找最小值,按照从小到大
if(flag>a[j]) {
flag = a[j];
num = j;
}
}
flag = a[i];
a[i] = a[num];
a[num] = flag;
}
//输出
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
例题3(插入排序)
对【2,2,1,6,4,9,7,6,8】进行插入排序。
思路分析:
- 先分为排序好和未排序两部分。
- 然后在未排序的数字中选出数字与前面排好序的数字进行对比。(与前一位进行对比)
- 然后将未排序的数字一一插入到排好序的数字的适当位置。
代码如下:
package test01;
public class Main_P11 {
public static void main(String[] args) {
int [] a = {2,2,1,6,4,9,7,6,8};
for (int i = 1; i < a.length; i++) {
for (int j = i; j > 0; j--) {
int flag = 0;
if(a[j-1]>a[j]) {
flag = a[j-1];
a[j-1] = a[j];
a[j] = flag;
}
}
}
//遍历输出
for (int i = 0; i < a.length; i++) {
System.out.print(a[i]+" ");
}
}
}
递归
什么是递归
函数/方法,直接/间接的调用自己。即:自己调用自己。一般在原来数据基础上进行加减乘除的操作。
简单例子:求1,2,3····100的累加和
package test01;
//递归实现
public class Draft {
public static void main(String[] args) {
System.out.println(sum(100));
}
public static int sum(int n) {
if(n == 1) {
return n;
}
else {
return n+sum(n-1); //递归调用
}
}
}
案例:斐波那契数列。如:1,1,2,3,5,8,13·····
前两个数是1,1,后面的数字是前两个数之和。这种数列就是斐波那契数列。
递归的特点
代码简洁,但涉及到的运算会随之递归的层数呈现指数级的增长。
递归题目的特点
怎么判断这道题是不是用递归:
- 题目难度太大。
- 没有明确的算法或者思路。
这个时候就要开始找规律了,试着用递归来计算。
递归和循环的关系
问题1:一道用递归解决的问题,那么它能用循环来解决吗?
答:可以。
问题2:一道用循环解决的问题,那么它能用递归来解决吗?
答:不一定。一部分可以,一部分不行。
特点:
- 循环相较于递归代码量变多了,但是消耗的资源,时间复杂度都降低了。
递归总结的心得
- 要有一个判断条件,如
if(n == 1){
return 1;
}
- 会调用自身,如:
return F(n)+F(n-1);
- 递归中出现的return 是开始返回的标志,则代码会一层一层返回,会执行调用自身代码后面部分的代码。而这里,往往是回溯的执行位置。
递归出现的问题
如果发现题目用递归超出时间限制,那怎么解决?
答:
- 换成循环来做。
- 加字典。(字典就是:将之前递归的结果存贮下来,然后递归调用的时候,直接去调用存贮得到的值) 常使用map来进行存储
怎么判断运行一段代码花费了多长时间?
答:[[JAVA 语法#计算代码运行时间]]
下面是一个将结果存储到map中的字典案例。
什么是map?答: [[数据结构#map]]
package test01;
import java.util.HashMap;
import java.util.Map;
public class Draft {
public static void main(String[] args) {
HashMap<Integer, Integer> map = new HashMap<>(); //map用来存储斐波那契数
int num = Fn(10, map); //存储第10位数字的值,即键10的值
System.out.println(num);
}
//斐波那契数列递归字典
public static int Fn(int n,Map<Integer, Integer> map) {
//判断有没有键位n的map,有的话直接返回键n的值
if(map.containsKey(n)) {
return map.get(n);
}
if(n == 1 || n == 2) {
return 1;
}
int result = Fn(n-1, map)+Fn(n-2, map); //用result保存斐波那契数的值
map.put(n, result); //将当前的斐波那契数存储到map中形成字典
return result; //返回斐波那契数
}
}
上述代码便是使用map来存储,成为字典。
例题(递归)
例题一(斐波那契数列)
计算第n位的斐波那契数列。假如:第10位。
思路分析:
- 斐波那契数除前两位以外,其他数都是由前面两位数字相加得到该位数。故使用递归求解
- 递归函数设计:判断第一位和第二位的时候返回 1 ;后面位数返回 n-1 和n-2 的和。
代码如下:
package test01;
public class Main_P12 {
public static void main(String[] args) {
System.out.println(Fn(10)); //求第10位斐波那契数
}
//斐波那契递归函数
public static int Fn(int n) {
if(n==1||n==2) {
return 1;
}
return Fn(n-1)+Fn(n-2);
}
}
例题二(递归)
思路分析:
- 第一行一列,第二行二列······分别是:1,5,13,25····
- 它们的规律是:两数之差分别为:4,8,12·····
- 则20行20列的数字就是这个数列中的第20位数字。
- 5 = 1×4 + 1;13 = 2 × 4+5;25 = 3×4+13 ···· F(n) = 4×(n-1)+F(n-1) 使用递归
代码如下:
package test01;
public class Main_P12_02 {
public static void main(String[] args) {
System.out.println(Fn(20));
}
//递归
public static int Fn(int n) {
if(n == 1) {
return 1;
}
return 4*(n-1)+Fn(n-1);
}
}
例题三(辗转相除法-求最大公约数)
定理:求两个数的最大公约数 = 其中小的那个数 和 两个数的余数 的最大公约数。
假如用gcd表示最大公约数,则公式如:
条件:a>b , 直到小数为0,则大数为最大公约数
gcd(a,b) = gcd(b,a%b)
如:
- 12 和 4 的最大公约数:就是4和0的最大公约数,小数为0,则4是最大公约数
- 10 和 8 的最大公约数:就是 8 和 10%8 = 2 ->是 2和8%2 = 0。即 2是最大公约数
题目:输入两个数,求最大公约数。
思路分析:
- 使用递归求解
- 当较小数为0,则较大数是最大公约数
- 每次得到两个数,即前两个数中的小数和前两个数的余数
代码如下:
package test01;
import java.util.Scanner;
public class Main_P14 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//输入
System.out.println("输入第一个数字:");
int n = scanner.nextInt();
System.out.println("输入第二个数字:");
int m = scanner.nextInt();
int result = Fn(n, m); //调用辗转相除法
//输出
System.out.println(result);
}
public static int Fn(int n,int m) {
//当有一个数为0,则返回大数是最大公约数
if( n == 0 || m == 0) {
if(n>m) {
return n;
}else {
return m;
}
}
//将较大数置为余数
if(n>m) {
n = n%m;
}else {
m = m%n;
}
return Fn(n, m);
}
}
全排列
- 求全排列:在n个元素中取n个元素,求所有的排列组合情况(每次取出全部的元素)
- 求组合:在n个元素里面取m个元素(m<=n),求所有个排列组合情况
- 求子集:n个元素的所有子集。
求全排列的常用方法
邻里交换法和回溯法
回溯法
求【1,2,3】的所有排列
【1,2,3】 【1,3,2】
【2,1,3】 【2,3,1】
【3,1,2】 【3,2,1】
回溯:一般用来解决搜索问题。
回溯
回溯就是类似于枚举,去一一尝试所有的解决可能性,当一个解有问题的或者不满足条件的时候就原路返回,去尝试其他的可能性,即回溯。
回溯算法一般写在递归的下面,就是当递归 return 之后,回去执行递归算法后面的代码,这个地方也就是回溯算法写的位置。
全排列
全排列用试探的方法列出所有可能性。一个长度为n的系列,它的全排列有 n! 种可能。
- 从集合中选取一个元素(有n种可能),并标记该元素已经使用。
- 在第一步的基础上递归到下一层,再在身下的n-1个元素中,然后按照第一步的方法再找一个元素。
- 依次类推,所有元素都被标记,将元素存起来取对比求解的情况。
邻里交换法
也是通过递归实现,但是是一种交换的思路。
步骤:
- 将数组分为两个部分,确定好的部分和未确定的部分。刚开始,都是未确认部分。
- 在没确认好的部分中,要使第一位可以和所有位进行交换(包括第一位)。然后将第一位确定
- 以此类推,将剩下部分的第一位也与其他能交换。
- 当所有位交换完成之后,数据与条件进行对比。对比结束后,还原数据。
示例如下图所示:
例题
例题一(回溯)
现在有一个三角形,一共有9个数字构成,每条边4个数字。现要求每条边的数字之和相等。请求出一共有多少中排列组合方式。(注意:镜像和旋转不算)
思路分析:
- 用递归来计算
- 结果要除以6(去掉每一种的镜像和旋转可能性)
- 使用两个数组,一个数组用来存储数据,另一个数组用来记录该位置是否被填充。
- 在回溯的时候将bool类型数组的位置置为false ,以便来填充数据
代码如下:
package test01;
public class Main_P16_02 {
static int num [] = new int[10]; //用来存放数字
static int count = 0 ; //用来记录符合条件的个数
static boolean [] bool = new boolean [10]; //辅助数组 标记数字是否被使用 注意:10个元素的数组,摒弃了下标为0的元素
public static void main(String[] args) {
dfs(1);
System.out.println(count/3/2); //除去镜像和旋转所得到重复的可能
}
//回溯算法
public static void dfs(int step) { //step代表第几位
//结束条件
if(step == 10) {
if(num[1]+num[2]+num[4]+num[6] == num[6]+num[7]+num[8]+num[9] &&
num[1]+num[2]+num[4]+num[6] == num[1]+num[3]+num[5]+num[9]) {
count++;
}
return;
}
//填数据
for (int i = 1; i < 10; i++) {
//当前位置没有填充数字
if(!bool[i]) { //bool[i]的值是false,而!bool[i] 的值是ture。且只有bool[i]为false,即该位置没有元素的时候,才会执行if判断内的代码
bool[i] = true;
num[step] = i;
//递归
dfs(step+1);
bool[i] = false;
}
}
}
}
例题一(邻里交换法)
题目与上题一致
现在有一个三角形,一共有9个数字构成,每条边4个数字。现要求每条边的数字之和相等。请求出一共有多少中排列组合方式。(注意:镜像和旋转不算)
思路分析:
- 使用邻里交换法
- 使用递归,然后交换元素来得到所有可能
代码如下:
package test01;
public class Main_P17_02 {
static int count; //记录一共有多少满足条件的
public static void main(String[] args) {
int [] num = {1,2,3,4,5,6,7,8,9};
dfs(num, 0);
System.out.println(count/6);
}
//递归
public static void dfs(int [] num , int index) {
//返回条件
if(index == 9) {
if(num[0]+num[1]+num[2]+num[3]==num[3]+num[4]+num[5]+num[6]&&num[0]+num[1]+num[2]+num[3]
==num[6]+num[7]+num[8]+num[0]) {
count++;
}
return;
}
for (int i = index; i < num.length; i++) {
//交换
swap(num, i, index);
//递归
dfs(num, index+1);
//清除交换
swap(num, i, index);
}
}
//交换函数
public static void swap(int [] num ,int i,int index) {
int x = num[i];
num[i] = num[index];
num[index] = x;
}
}
例题一(暴力破解)
现在有一个三角形,一共有9个数字构成,每条边4个数字。现要求每条边的数字之和相等。请求出一共有多少中排列组合方式。(注意:镜像和旋转不算)
思路分析:
- 一共有9位数字,使用9个for循环。
- 当每一位上面数字相同的时候,代码继续continue
- 当满足条件的时候count++
代码如下:
package test01;
public class Main_P18 {
public static void main(String[] args) {
int count = 0; //用于记录一共有多少种可能
for(int a = 1;a<=9;a++) {
for(int b = 1;b<=9;b++) {
for(int c = 1;c<=9;c++) {
for(int d = 1;d<=9;d++) {
for(int e = 1;e<=9;e++) {
for(int f = 1;f<=9;f++) {
for(int g = 1;g<=9;g++) {
for(int h =1 ;h<=9;h++) {
for(int i = 1;i<=9;i++) {
//如果出现相同数字则跳过
if(a==b||a==c||a==d||a==e||a==f||a==g||a==h||a==i||
b==c||b==d||b==e||b==f||b==g||b==h||b==i||
c==d||c==e||c==f||c==g||c==h||c==i||
d==e||d==f||d==g||d==h||d==i||
e==f||e==g||e==h||e==i||
f==g||f==h||f==i||
g==h||g==i||
h==i) {
continue;
}
//如果符合条件
if(a+b+c+d==d+e+f+g && a+b+c+d==g+h+i+a) {
count++;
}
}
}
}
}
}
}
}
}
}
System.out.println(count/6);
}
}
以上便是我在学习算法 排序算法,递归,回溯··学习的笔记和遇到问题的解决思路,笔记内容通俗易懂,包含了概念,理解,自己的心得和题目。可以帮助你很好的掌握这方面的知识。以后还会多更新类似的内容,希望大家多多关注!!!