数据结构与算法weeks02

       栈、递归排序、查找
       
在这里插入图片描述
开始~~~



一、栈

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)介绍
      &nbsp希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
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~~,查找算法,哈希表,树
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值