栈、递归排序、查找
开始~~~
一、栈
1.栈(stack)的介绍
1) 栈是一个先入后出(FILO-First In Last Out)的有序列表。
2)允许插入和删除的一端为栈顶(Top),固定的一端称为栈底(Bottom)。
3) 入栈示意图
4) 出栈示意图
2.栈的应用场景
1) 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
2)处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
3) 表达式的转换[中缀表达式转后缀表达式]与求值。
4) 二叉树的遍历。
3.中缀表达式转后缀表达式
3.1中缀表达式
1)中缀表达式就是常见的运算表达式,如(3+4)×5-6
2)中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作。因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
3.2后缀表达式
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。
比如:
3.3中缀–后缀
1.中缀转后缀的步骤
1)初始化两个栈:运算符栈s1和储存中间结果的栈s2;
2)从左至右扫描中缀表达式;
3)遇到操作数时,将其压s2;
4)遇到运算符时,比较其与s1栈顶运算符的优先级:
①若优先级不大于栈顶运算符,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶运算符相比较;
②否则,直接入栈。
5)遇到括号时:
①如果是左括号“(”,则直接压入s1
②如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
6)重复步骤2至5,直到表达式的最右边
7)将s1中剩余的运算符依次弹出并压入s2
8)依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
2.代码
1)将表达式转换为集合
//方法:将中缀表达式转成对应的List
public static List<String> toList(String s){
//定义一个List,存放中缀表达式对应的内容
ArrayList<String> list = new ArrayList<String>();
int i = 0;//一个指针,用于遍历中缀表达式字符串
String str;//对多位数的拼接
char c;//每遍历一个字符,就放入到c
do{
//如果c是一个非数字,需要加入到ls
if((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57){
list.add("" + c);
i++;
}else{//如果是一个数,需要考虑多位数
str = "";//先将str置成"",'0'[48]->'9'[57]
while(i < s.length() && (c = s.charAt(i)) > 48 && (c = s.charAt(i)) <= 57){
str += c;//拼接
i++;
}
list.add(str);
}
}while(i < s.length());
return list;
}
2)比较优先级
//返回优先级
class Operation{
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 1;
private static int DIV = 1;
//写一个方法,返回对应的优先级数字
public static int getValue(String operation){
int result = 0;
switch (operation){
case "+":
result = ADD;
break;
case "-":
result = SUB;
break;
case "*":
result = MUL;
break;
case "/":
result = DIV;
break;
default :
break;
}
return result;
}
}
3)中缀转后缀
public static List<String> middleToLast(List<String> ls){
//定义一个符号栈
Stack<String> s1 = new Stack<String>();
//因此直接使用ArrayList
ArrayList<String> s2 = new ArrayList<String>();
for(String item : ls){
//如果是一个数,加入s2
if(item.matches("\\d+")){
s2.add(item);
}else if(item.equals("(")){
s1.push(item);
}else if(item.equals(")")){
//如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
while(!s1.peek().equals("(")){
s2.add(s1.pop());
}
s1.pop();//将(弹出s1栈,消除小括号
}else{
//当item的优先级小于等于s1栈顶的运算符,将s1栈顶的运算符弹出并加入到s2中,再次转到(4.1)与s1中新的栈顶运算符相比较
while(s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)){
s2.add(s1.pop());
}
//还需要将item压入栈
s1.push(item);
}
}
//将s1中剩余的运算依次弹出并加入s2
while(s1.size() != 0){
s2.add(s1.pop());
}
return s2;
}
4.逆波兰计算器
逆波兰计算器实际上就是计算后缀表达式的结果。
4.1思路分析
依次遍历后缀表达式,如果遇到操作数则入栈;如果遇到运算符则从栈中pop出两个数分别为:num2和num1,然后num1 (±*/) num2将结果入栈,直到遍历完表达式,栈中只剩下一个数,即为结果。
4.2代码
public void calculate(List<String> list){
Stack<String> stack = new Stack<>();
for (String l : list){
if (l.matches("\\d+")){
stack.push(l);
}else {
int res = 0;
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
res = cal(l, num1, num2);
stack.push("" + res);
}
}
System.out.println(stack.pop());
}
二、递归
看起来简单,理解起来很难,不过debug可以解决一切~~
1.递归的概念、解决的问题、规则
1.1概念
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
1.2递归能解决的问题
1)各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)。
2)各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等。
1.3规则
递归需要遵守的重要规则:
1)执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
2)方法的局部变量是独立的,不会相互影响。
3)如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据。
4)递归必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError)
5)当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
2.递归应用实例
迷宫和八皇后问题
2.1迷宫问题
2.1.1思路分析
2.1.2代码
public boolean findWay(int[][] map,int i,int j){
if (map[6][5] == 2){
for(int p = 0;p < 8;p++){
for(int q = 0;q < 7;q++){
System.out.print(map[p][q] + " ");
}
System.out.println();
}
return true;
}else{
if (map[i][j] == 0){
map[i][j] = 2;
if (findWay(map,i + 1,j)){
return true;
}else if (findWay(map,i,j + 1)){
return true;
}else if (findWay(map,i - 1,j)){
return true;
}else if (findWay(map,i,j - 1)){
return true;
}else {
map[i][j] = 3;
return false;
}
}else {
return false;
}
}
}
2.2八皇后问题
2.2.1问题介绍
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法
2.2.2思路分析
1)第一个皇后先放第一行第一列。
2)第二个皇后放在第二行第一列、然后判断是否OK, 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适的位置。
3)继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。
4)当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到。
5)然后回头继续第一个皇后放第二列,后面继续循环执行 1,2,3,4的步骤。
说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题.
arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行,即第几个皇后,
arr[i] = val , val 表示第i+1个皇后,放在第i+1行的第val+1列
2.2.3代码
//放置第n个棋子
public void putCheese(int n){
if (n == 8){
for(int i : arr){
System.out.print(i + " ");
}
System.out.println();
return;
}else{
for (int i = 0;i < 8;i++){
arr[n] = i;
if (judge(n)){
putCheese(n + 1);
}
}
}
}
public boolean judge(int n){
for (int i = 0;i < n;i++){
if (arr[i] == arr[n] || Math.abs(i - n) == Math.abs(arr[i] - arr[n])){
return false;
}
}
return true;
}
三、排序算法
1.排序算法的介绍
1)介绍:先排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
2)排序的分类:
① 内部排序:
需要处理的所有数据都加载到内部存储器中进行排序。
②外部排序法:
数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
3)常见的排序算法:
2.算法的复杂度
2.1概念
算法的时间复杂度:度量一个程序(算法)执行时间的两种方法
2.2分类
1)事后统计的方法
这种方法可行, 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;
二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。
2)事前估算的方法
通过分析某个算法的时间复杂度来判断哪个算法更优。
2.3时间频度
1)基本介绍一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)
2)举例
3)忽略常数项、忽略低次项、忽略系数
2.4时间复杂度
1.时间复杂度
1)一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) )为算法的渐进时间复杂度,简称时间复杂度。
2)T(n) 不同,但时间复杂度可能相同 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n)不同,但时间复杂度相同,都为O(n²)。
3)计算时间复杂度的方法:
用常数1代替运行时间中的所有加法常数
2.常见的时间复杂度
1)常数阶O(1):无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)。
说明:上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2)对数阶O(log2n)
说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n也就是说当循环 log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(log2n) 。 O(log2n) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(log3n)
3)线性阶O(n)
说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
4)线性对数阶O(nlogN)
说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
5)平方阶O(n²)
说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(nn),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(mn)
6)立方阶O(n³)、K次方阶O(n^k)
说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似
2.5算法的空间复杂度
1)类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
2)空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况
3)在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间.
3.排序算法
3.1冒泡排序
1)介绍
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
2)说明
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。
3)思路分析
一共进行数组长度大小-1次的大循环,每次小循环的次数都在减小,每一轮都将大数放到数组最后。
4)代码:
public void BubbleSort(){
int temp = 0;
boolean isEnd = false;
for (int i = 0;i < arr.length - 1;i++){
for (int j = 0;j < arr.length - 1 - i;j++){
if (arr[j] > arr[j + 1]){
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
isEnd = true;
}
}
if (!isEnd){
break;
}else{
isEnd = false;
}
}
}
3.2选择排序
1)介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
2)思路分析
找最小原则;同样也是两层的for循环;每一轮循环都默认数组最前面的为最小值,然后依次遍历,找到最小的值以及索引,然后与数组最前面的值交换。
3)代码:
public void selectSort(){
int minIndex;
int min;
for (int i = 0; i < arr.length - 1; i++) {
minIndex = i;
min = arr[i];
for (int j = i + 1;j < arr.length;j++){
if (min > arr[j]){
min = arr[j];
minIndex = j;
}
}
if (minIndex != i){
arr[minIndex] = arr[i];
arr[i] = min;
}
}
}
3.3插入排序
1)介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
2)思路分析
插入排序(Insertion Sorting)的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
3)代码:
public void insertSort(){
int insertValue;
int insertIndex;
for (int i = 1;i < arr.length;i++){
insertValue = arr[i];
insertIndex = i - 1;
while (insertIndex >= 0 && insertValue< arr[insertIndex]){
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
arr[insertIndex + 1] = insertValue;
}
}
3.4希尔排序法
针对插入排序存在的问题
1)介绍
 希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
2)思路分析
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
3)代码
public void shellSort(){
int insertValue;
int insertIndex;
for(int gap = arr.length / 2;gap > 0;gap /= 2){
for (int i = gap;i < arr.length;i++){
insertValue = arr[i];
insertIndex = i - gap;
while (insertIndex >= 0 && insertValue < arr[insertIndex]){
arr[insertIndex + gap] = arr[insertIndex];
insertIndex -= gap;
}
arr[insertIndex + gap] = insertValue;
}
}
}
3.5快速排序法
1)介绍
快速排序是对冒泡排序的一种改进。
2)思路分析
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
3)代码
public void quickSort(int left,int right){
int l = left;
int r = right;
int temp = 0;
int pivot = arr[(right + left) / 2];
while (l < r){
while (arr[l] < pivot){
l += 1;
}
while (arr[r] > pivot){
r -= 1;
}
if (l >= r){
break;
}
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
if (arr[l] == pivot){
r--;
}
if (arr[r] == pivot){
l++;
}
}
if (l == r){
l += 1;
r -= 1;
}
if (left < r){
quickSort(left,r);
}
if (right > l){
quickSort(l,right);
}
}
3.6归并排序法
1)介绍
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略。
2)思路分析
3)代码
//分解
public void mergeSort(int left,int right){
if (left < right){
int mid = (left + right) / 2;
mergeSort(left,mid);
mergeSort(mid + 1,right);
merge(left,mid,right);
}
}
//合并
public void merge(int left,int mid,int right){
int i = left;
int j = mid + 1;
int t = 0;
while (i <= mid && j <= right){
if (arr[i] < arr[j]){
temp[t] = arr[i];
t += 1;
i += 1;
}else{
temp[t] = arr[j];
t += 1;
j += 1;
}
}
while (i <= mid){
temp[t] = arr[i];
i += 1;
t += 1;
}
while (j <= right){
temp[t] = arr[j];
j += 1;
t += 1;
}
int l = left;
t = 0;
while (l <= right){
arr[l] = temp[t];
l += 1;
t += 1;
}
}
3.7基数排序
1)介绍
是桶排序的扩展。将整数按位数切割成不同的数字,然后按每个位数分别比较。
2)思路分析
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
3)代码
public void radixSort(){
int max = 0;
for (int i = 0;i < arr.length;i++){
if (max < arr[i]){
max = arr[i];
}
}
int[][] bucket = new int[10][arr.length];
int[] bucketEleCount = new int[10];
int loop = ("" + max).length();
int index = 0;
for (int i = 0,n = 1; i < loop; i++,n*=10) {
for (int j = 0;j < arr.length;j++){
//根据个位数字添加到对应的桶中
int num = arr[j] /n % 10;
bucket[num][bucketEleCount[num]] = arr[j];
bucketEleCount[num]++;
}
//从桶中取出数据
for(int k = 0;k < bucketEleCount.length;k++){
if(bucketEleCount[k] != 0){
//循环第k个桶,取出其中的数据
for(int l = 0;l < bucketEleCount[k];l++){
//取出元素放入到arr中
arr[index] = bucket[k][l];
index++;
}
}
//置0
bucketEleCount[k] = 0;
}
index = 0;
}
}
4)说明
①基数排序是对传统桶排序的扩展,速度很快.
②基数排序是经典的空间换时间的方式,占用内存很大, 当对海量数据排序时,容易造成 OutOfMemoryError 。
③基数排序时稳定的。
3.8常用排序对比
总结
To be continued~~,查找算法,哈希表,树