1.了解基本数据结构及特点
如,有哪些二叉树,各有什么特点
树
二叉搜索树
每个节点都包含一个值,每个节点至多有两棵子树,左孩子小于自己,右孩子大于自己,时间复杂度是O(log(n)),随着不断插入节点,二叉树树高变大,当只有左(右)孩子时,时间复杂度变为O(n).
平衡二叉树
保证每个节点左右子树高度差绝对值不超过1.
比如,AVL树在插入和删除数据是经常需要旋转以保持平衡.适合插入删除少场景.
红黑树
非严格平衡二叉树,更关注局部平衡,而非总体平衡,没有一条路径比其他路径长出两倍,接近平衡,减少了许多不必要的旋转操作,更加实用.
特点:
每个节点不是红就是黑
根节点是黑色
每个叶子都是黑色空节点
红色节点的子节点都是黑的
任意节点到其叶节点的每条路径上存在的黑色节点数量相同.
B树
适用于文件索引,优先减少磁盘IO次数,最大子节点称为B树的阶
m阶b树特点:
非叶子节点最多有m棵子树
根节点最少有两棵子树
非根非叶节点最少有m/2棵子树
非子叶节点保存的关键字个数,为该节点子树个数减一
非叶子节点的关键字大小有序
关键字的左孩子都小于该关键字,右孩子都大于关键字
所有叶节点都在同一层
采用二分查找法
B+树
定义与b树基本相同,
区别:
节点有多少关键字,有多少子树
关键字对应子树的节点都大于等于关键字,子树中包括关键字自身
所有关键字都出现在叶节点中
所有叶节点都有指向下一个叶节点的指针
搜索时只会命中叶节点,叶子节点相当于数据存储层,保存关键字对应的数据,非叶节点只保存关键字与指向叶节点的指针
B+树比B树更适合做索引:
叶节点之间有指针相连,B+树跟适合范围检索
由于非叶节点只保留关键字和指针,B+树可以容纳更多的关键字,降低树高,磁盘IO代价更低
B+树查询过程稳定,必须由根节点到叶节点,所有关键字查询路径相同,效率相当.Mysql数据可得索引就提供了B+树的实现
B*树
在B+树的非叶节点上增加了指向同一层下一个非叶节点的指针
2.表/栈/队列/树需要熟练掌握,深刻理解使用场景
红黑树适合用搜索,B+数适合做索引
数据结构
数组/栈/队列/链表/树/哈希表/堆/图
数组
冒泡/选择/插入排序算法
无序数组的优点:插入快,如果知道下标,可以很快的存取
无序数组的缺点:查找慢,删除慢,大小固定。
有序数组
所谓的有序数组就是指数组中的元素是按一定规则排列的,其好处就是在根据元素值查找时可以是使用二分查找,查找效率要比无序数组高很多,在数据量很大时更加明显。当然缺点也显而易见,当插入一个元素时,首先要判断该元素应该插入的下标,然后对该下标之后的所有元素后移一位,才能进行插入,这无疑增加了很大的开销。
因此,有序数组适用于查找频繁,而插入、删除操作较少的情况
有序数组最大的优势就是可以提高查找元素的效率,在上例中,find方法使用了二分查找法,
栈是后进先出,而队列刚好相反,是先进先出
链表是一种插入和删除都比较快的数据结构,缺点是查找比较慢。除非需要频繁的通过下标来随机访问数据,否则在很多使用数组的地方都可以用链表代替
在链表中,每个数据项都包含在“链结点”中,一个链结点是某个类的对象。每个链结点对象中都包含一个对下一个链接点的引用,链表本身的对象中有一个字段指向第一个链结点的引用,
在数组中,每一项占用一个特定的位置,这个位置可以用一个下标号直接访问,就像一排房子,你可以凭房间号找到其中特定的意见。在链表中,寻找一个特定元素的唯一方法就是沿着这个元素的链一直找下去,知道发现要找的那个数据项
单链表
链表的删除指定链结点,是通过将目标链结点的上一个链结点的next指针指向目标链结点的下一个链结点,
通过这种方法,在链表的指针链中将跳过与删除的元素,达到删除的目的。不过,实际上删除掉的元素的next指针还是指向原来的下一个元素的,只是它不能再被单链表检索到而已,JVM的垃圾回收机制会在一定的时间之后回收它
添加链结点比删除复杂一点,首先我们要使插入位置之前的链结点的next指针指向目标链结点,其次还要将目标链结点的next指针指向插入位置之后的链结点。本例中的插入位置仅限于链表的第一个位置,
3.了解常用的搜索/排序算法,以及复杂度和稳定性
特别是快速排序和堆排序
快速排序
把整个序列看做一个数组,把第零个位置看做中轴,和最后一个比,如果比它小交换,比它大不做任何处理;交换了以后再和小的那端比,比它小不交换,比他大交换。这样循环往复,一趟排序完成,左边就是比中轴小的,右边就是比中轴大的,然后再用分治法,分别对这两个独立的数组进行排序。
快排原理:
在要排的数(比如数组A)中选择一个中心值key(比如A[0]),通过一趟排序将数组A分成两部分,其中以key为中心,key右边都比key大,key左边的都key小,然后对这两部分分别重复这个过程,直到整个有序。
public int getMiddle(Integer[] list, int low, int high) {
int tmp = list[low]; //数组的第一个作为中轴
while (low < high) {
while (low < high && list[high] > tmp) {
high--;
}
list[low] = list[high]; //比中轴小的记录移到低端
while (low < high && list[low] < tmp) {
low++;
}
list[high] = list[low]; //比中轴大的记录移到高端
}
list[low] = tmp; //中轴记录到尾
return low; //返回中轴的位置
}
public class QuickSort {
public static void quickSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>high){
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
quickSort(arr, 0, arr.length-1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
堆排序是一种树形选择排序方法,它的特点是:在排序的过程中,将array[0,...,n-1]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素。
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序基本思想及步骤
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
步骤一 构造初始堆。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。
步骤二 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
package dui;
import java.util.Arrays;
public class HeapSort {
public static void main(String []args){
int []arr = {3,1,4,2,8,5,9,7,6};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
4.了解常用的字符串处理算法
Example:判断给定字符串中的符号是否匹配
解题思路:
1. 使用栈
2. 遇到左括号入栈
3. 与右括号出栈,判断出栈括号是否成对
private static fianl Map<Character,Character> brackets = new HashMap<>();
static{
brackets.put(‘)‘,‘(‘);
brackets.put(‘]‘,‘[‘);
brackets.put(‘}‘,‘{‘);
}
public static boolean isMatch(String str){
if(str==null){
return false;
}
Stack<Character> stack = new stack<>();
for(char ch : str.toCharArray()){
if(barckets.containsValue(ch)){
stack.put(che);
} else if (brackets.contiansKey(ch)){
if(stack.empty() || stack.pop() != bracjets.get(ch)){
return false;
}
}
}
return stack.empty();
}
解题技巧
认真审题:
单模匹配还是多模匹配
时间复杂度空间复杂度是否有要求
明确期望的返回值,如,多匹配结果的处理
解题思路
单模匹配:BM,KMP算法
多模匹配:Tire树
前缀或后缀匹配
可以借助栈,树等数据结构
如,BM使用后缀匹配进行字符串匹配
在用于查找子字符串的算法中,BM(Boyer-Moore)算法是当前有效且应用比较广泛的一种算法,各种文本编辑器的“查找”功能(Ctrl+F),大多采用Boyer-Moore算法。比我们学习的KMP算法快3~5倍。
我们把被搜索的字符串称为文本text,待匹配的字符串称为模式串pattern。BM算法的核心思想就是两个,第一是坏字符,第二是好后缀,好后缀就是pattern与text从右往左连续匹配成功的子串,坏字符就是pattern与text从右往左第一个匹配失败的在text中的字符,
对于坏字符和好后缀,无非就是不同的模式串移动规则,通过各自不同的移动规则,确定分别对于坏字符和好后缀模式串需要移动的位数,最终选择移动位数最大的进行移动即可。
一般来说,文本和模式串都是用数组来表示的,在字符串的匹配过程中,当出现不匹配时,需要进行模式串相对于文本的移动,我们简称为模式串的移动。
1、坏字符规则
后移位数 = 坏字符的位置 - 模式串中的坏字符上一次出现位置
如果"坏字符"不包含在模式串之中,则上一次出现位置为 -1。
2、好后缀规则
后移位数 = 好后缀的位置 - 模式串中的上一次出现位置
package com.buaa;
import java.util.Random;
/**
* @ProjectName StringPatternMatchAlgorithm
* @PackageName com.buaa
* @ClassName BM
* @Description TODO
*/
public class BM {
/**
* 利用坏字符规则计算移动位数
*/
public static int badCharacter(String moduleString, char badChar,int badCharSuffix){
return badCharSuffix - moduleString.lastIndexOf(badChar, badCharSuffix);
}
/**
* 利用好后缀规则计算移动位数
*/
public static int goodCharacter(String moduleString,int goodCharSuffix){
int result = -1;
// 模式串长度
int moduleLength = moduleString.length();
// 好字符数
int goodCharNum = moduleLength -1 - goodCharSuffix;
for(;goodCharNum > 0; goodCharNum--){
String endSection = moduleString.substring(moduleLength - goodCharNum, moduleLength);
String startSection = moduleString.substring(0, goodCharNum);
if(startSection.equals(endSection)){
result = moduleLength - goodCharNum;
}
}
return result;
}
/**
* BM匹配字符串
*
* @param originString 主串
* @param moduleString 模式串
* @return 若匹配成功,返回下标,否则返回-1
*/
public static int match(String originString, String moduleString){
// 主串
if (originString == null || originString.length() <= 0) {
return -1;
}
// 模式串
if (moduleString == null || moduleString.length() <= 0) {
return -1;
}
// 如果模式串的长度大于主串的长度,那么一定不匹配
if (originString.length() < moduleString.length()) {
return -1;
}
int moduleSuffix = moduleString.length() -1;
int module_index = moduleSuffix;
int origin_index = moduleSuffix;
for(int ot = origin_index; origin_index < originString.length() && module_index >= 0;){
char oc = originString.charAt(origin_index);
char mc = moduleString.charAt(module_index);
if(oc == mc){
origin_index--;
module_index--;
}else{
// 坏字符规则
int badMove = badCharacter(moduleString,oc,module_index);
// 好字符规则
int goodMove = goodCharacter(moduleString,module_index);
// 下面两句代码可以这样理解,主串位置不动,模式串向右移动
origin_index = ot + Math.max(badMove, goodMove);
module_index = moduleSuffix;
// ot就是中间变量
ot = origin_index;
}
}
if(module_index < 0){
// 多减了一次
return origin_index + 1;
}
return -1;
}
/**
* 随机生成字符串
*
* @param length 表示生成字符串的长度
* @return String
*/
public static String generateString(int length) {
String baseString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuilder result = new StringBuilder();
Random random = new Random();
for (int i = 0; i < length; i++) {
result.append(baseString.charAt(random.nextInt(baseString.length())));
}
return result.toString();
}
public static void main(String[] args) {
// 主串
// String originString = generateString(10);
String originString = "HERE IS A SIMPLE EXAMPLE";
// 模式串
// String moduleString = generateString(4);
String moduleString = "EXAMPLE";
// 坏字符规则表
// int[] badCharacterArray = badCharacter(originString,moduleString);
System.out.println("主串:" + originString);
System.out.println("模式串:" + moduleString);
int index = match(originString, moduleString);
System.out.println("匹配的下标:" + index);
}
}
5.能够分析算法实现的复杂度
特别是时间复杂度
O(1)
O(1)是常量级时间复杂度的一种表示方法,并非只执行一行代码
代码执行时间不是随着n的增大而增大,这样的代码的时间复杂度都是O(1)
通常只要算法中不存在循环、递归,即使代码有很多行,时间复杂度仍是O(1)
②O(logn)、O(nlogn)对数阶时间复杂度
这段代码的第3行是执行次数最多的,只要算出第3行执行的次数,就是整个代码的时间复杂度。
i从1开始取值,每一次循环乘以2.可以看到 i=i*2是一个等比数列
我们只要算出x是多少,就是执行的次数了 2^x=n -->x=log2n,所以时间复杂度应该为O(log2n)
很容易就能看出来,应该是O(log3n)。
但是上面的O(log2n)和O(log3n)可以通过换底公式换成以2为底的对数,且可以忽略系数,所以都记做 O(logn)。
关于O(nlogn),就是把上面的代码在循环执行n遍了。其中归并排序、快速排序的时间复杂度就是O(nlogn)
③O(m+n)、O(m*n)
1. 加法法则(量级最大法则):总复杂度等于量级最大的那段代码的复杂度
同理,sum_2和sum_3分别是 O(n)和O(n^2),对于这三个,我们取量级最大的O(n^2),所以总的时间复杂度就等于量级最大的那段代码的时间复杂度。
2.乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
f()函数的时间复杂度是 T1(n)=O(n),如果先把f()函数看成简单的操作,则cal()函数的时间复杂度是T2(n)=O(n),所以整个cal()函数的时间复杂度是T(n)=T2(n)*T1(n)=O(n*n)=O(n^2)
一个经验规则:
其中c是一个常量,如果一个算法的复杂度为c 、 log2n 、n 、 nlog2 、 n* ,那么这个算法时间效率比较高 ,
如果是2n** ,3n** ,n!,那么稍微大一些的n就会令这个算法不能动了,居于中间的几个则差强人意。
TopK问题
找出N个数中最小的k个数(N非常大)
解法:
用前K个数创建大小为K的大根堆
剩余的N-K个数与堆顶进行比较
时间复杂度:O(N*log(K))
优点:不用在内存中读入所有元素,适用于非常大的数据集
从N有序队列中找到最小的K个值
解法:
用N个队列的最小值组成大小为K的小根堆
取堆顶值
将堆顶值所在队列的下个值加入堆(与堆中最大值比较,若该值大于最大值则可停止循环)
重复步骤2,直到K次
时间复杂度:O((N+K-1)*log(K))
1.能够将数据结构与实际使用场景结合
如,介绍红黑树时,结合TreeMap的实现,介绍B+数时,结合Mysql中的索引实现
TreeMap是基于红黑树的实现,也是记录了key-value的映射关系,该映射根据key的自然排序进行排序或者根据构造方法中传入的比较器进行排序,也就是说TreeMap是有序的key-value集合。
通过TreeMap的定义可以看出以下几点:
1.TreeMap的内部实现是红黑树实现的,
2.TreeMap是有序的key-value集合。
mysql的B+树 索引
7.mysql索引底层数据结构与算法
索引的数据结构 二叉树/HASH/BTREE
Btree 度(Degree)-节点的数据存储个数 横向变长,高度变少,节点查找是在内存里
一次IO是4K数据,节点的度就是4K数据
B+Tree 非叶子节点不存储data,只存储key,可以增大度
一般使用磁盘IO次数评价索引结构的优劣
myisam索引实现 存储引擎是表级别
索引和数据是分离的 叶子节点存的是文件指针不是数据
主键索引/非主键索引/分开存储的
innodb 主键索引 数据文件本身就是索引文件 叶子节点存储就是数据
innodb必须要有主键 整型自增主键
非主键索引叶子节点存储的是主键,并不是数据,需要查找2次才能找到数据
联合索引的底层存储结构同上
真题汇总
1.各种排序算法的实现和复杂度,稳定性
这个有个表
2.二叉树的前中后序遍历
1.前序遍历
前序遍历(DLR,lchild,data,rchild),是二叉树遍历的一种,也叫做先根遍历、先序遍历、前序周游,可记做根左右。前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。
前序遍历首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树。
若二叉树为空则结束返回,否则:
(1)访问根结点。
(2)前序遍历左子树。
(3)前序遍历右子树 。前序遍历
需要注意的是:遍历左右子树时仍然采用前序遍历方法。
如右图所示二叉树
前序遍历结果:ABDECF
已知后序遍历和中序遍历,就能确定前序遍历。
其实在遍历二叉树的时候有三次遍历, 比如前序遍历:A->B->D->D(D左子节点并返回到D)->D(D右子节点并返回到D)->B->E->E(左)->E(右)->->B->A->C->F->F(左)->F(右)->C->C(右),所以可以用栈结构,把遍历到的节点压进栈,没子节点时再出栈。也可以用递归的方式,递归的输出当前节点,然后递归的输出左子节点,最后递归的输出右子节点。直接看代码更能理解:
package test;
//前序遍历的递归实现与非递归实现
import java.util.Stack;
public class Test
{
public static void main(String[] args)
{
TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
for(int i = 0; i < 10; i++)
{
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++)
{
if(i*2+1 < 10)
node[i].left = node[i*2+1];
if(i*2+2 < 10)
node[i].right = node[i*2+2];
}
preOrderRe(node[0]);
}
public static void preOrderRe(TreeNode biTree)
{//递归实现
System.out.println(biTree.value);
TreeNode leftTree = biTree.left;
if(leftTree != null)
{
preOrderRe(leftTree);
}
TreeNode rightTree = biTree.right;
if(rightTree != null)
{
preOrderRe(rightTree);
}
}
public static void preOrder(TreeNode biTree)
{//非递归实现
Stack<TreeNode> stack = new Stack<TreeNode>();
while(biTree != null || !stack.isEmpty())
{
while(biTree != null)
{
System.out.println(biTree.value);
stack.push(biTree);
biTree = biTree.left;
}
if(!stack.isEmpty())
{
biTree = stack.pop();
biTree = biTree.right;
}
}
}
}
class TreeNode//节点结构
{
int value;
TreeNode left;
TreeNode right;
TreeNode(int value)
{
this.value = value;
}
}
2.中序遍历
中序遍历(LDR)是二叉树遍历的一种,也叫做中根遍历、中序周游。在二叉树中,先左后根再右。巧记:左根右。
中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树
若二叉树为空则结束返回,
否则:
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
如右图所示二叉树
中序遍历结果:DBEAFC
import java.util.Stack;
public class Test
{
public static void main(String[] args)
{
TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
for(int i = 0; i < 10; i++)
{
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++)
{
if(i*2+1 < 10)
node[i].left = node[i*2+1];
if(i*2+2 < 10)
node[i].right = node[i*2+2];
}
midOrderRe(node[0]);
System.out.println();
midOrder(node[0]);
}
public static void midOrderRe(TreeNode biTree)
{//中序遍历递归实现
if(biTree == null)
return;
else
{
midOrderRe(biTree.left);
System.out.println(biTree.value);
midOrderRe(biTree.right);
}
}
public static void midOrder(TreeNode biTree)
{//中序遍历费递归实现
Stack<TreeNode> stack = new Stack<TreeNode>();
while(biTree != null || !stack.isEmpty())
{
while(biTree != null)
{
stack.push(biTree);
biTree = biTree.left;
}
if(!stack.isEmpty())
{
biTree = stack.pop();
System.out.println(biTree.value);
biTree = biTree.right;
}
}
}
}
class TreeNode//节点结构
{
int value;
TreeNode left;
TreeNode right;
TreeNode(int value)
{
this.value = value;
}
}
3.后序遍历(难点)
后序遍历(LRD)是二叉树遍历的一种,也叫做后根遍历、后序周游,可记做左右根。后序遍历有递归算法和非递归算法两种。在二叉树中,先左后右再根。巧记:左右根。
后序遍历首先遍历左子树,然后遍历右子树,最后访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点。即:
若二叉树为空则结束返回,
否则:(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点
如右图所示二叉树
后序遍历结果:DEBFCA
已知前序遍历和中序遍历,就能确定后序遍历。
算法核心思想:
首先要搞清楚先序、中序、后序的非递归算法共同之处:用栈来保存先前走过的路径,以便可以在访问完子树后,可以利用栈中的信息,回退到当前节点的双亲节点,进行下一步操作。
后序遍历的非递归算法是三种顺序中最复杂的,原因在于,后序遍历是先访问左、右子树,再访问根节点,而在非递归算法中,利用栈回退到时,并不知道是从左子树回退到根节点,还是从右子树回退到根节点,如果从左子树回退到根节点,此时就应该去访问右子树,而如果从右子树回退到根节点,此时就应该访问根节点。所以相比前序和后序,必须得在压栈时添加信息,以便在退栈时可以知道是从左子树返回,还是从右子树返回进而决定下一步的操作。
import java.util.Stack;
public class Test
{
public static void main(String[] args)
{
TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
for(int i = 0; i < 10; i++)
{
node[i] = new TreeNode(i);
}
for(int i = 0; i < 10; i++)
{
if(i*2+1 < 10)
node[i].left = node[i*2+1];
if(i*2+2 < 10)
node[i].right = node[i*2+2];
}
postOrderRe(node[0]);
System.out.println("***");
postOrder(node[0]);
}
public static void postOrderRe(TreeNode biTree)
{//后序遍历递归实现
if(biTree == null)
return;
else
{
postOrderRe(biTree.left);
postOrderRe(biTree.right);
System.out.println(biTree.value);
}
}
public static void postOrder(TreeNode biTree)
{//后序遍历非递归实现
int left = 1;//在辅助栈里表示左节点
int right = 2;//在辅助栈里表示右节点
Stack<TreeNode> stack = new Stack<TreeNode>();
Stack<Integer> stack2 = new Stack<Integer>();//辅助栈,用来判断子节点返回父节点时处于左节点还是右节点。
while(biTree != null || !stack.empty())
{
while(biTree != null)
{//将节点压入栈1,并在栈2将节点标记为左节点
stack.push(biTree);
stack2.push(left);
biTree = biTree.left;
}
while(!stack.empty() && stack2.peek() == right)
{//如果是从右子节点返回父节点,则任务完成,将两个栈的栈顶弹出
stack2.pop();
System.out.println(stack.pop().value);
}
if(!stack.empty() && stack2.peek() == left)
{//如果是从左子节点返回父节点,则将标记改为右子节点
stack2.pop();
stack2.push(right);
biTree = stack.peek().right;
}
}
}
}
class TreeNode//节点结构
{
int value;
TreeNode left;
TreeNode right;
TreeNode(int value)
{
this.value = value;
}
}
4.层次遍历
与树的前中后序遍历的DFS思想不同,层次遍历用到的是BFS思想。一般DFS用递归去实现(也可以用栈实现),BFS需要用队列去实现。
层次遍历的步骤是:
1.对于不为空的结点,先把该结点加入到队列中
2.从队中拿出结点,如果该结点的左右结点不为空,就分别把左右结点加入到队列中
3.重复以上操作直到队列为空
public static void levelOrder(TreeNode biTree)
{//层次遍历
if(biTree == null)
return;
LinkedList<TreeNode> list = new LinkedList<TreeNode>();
list.add(biTree);
TreeNode currentNode;
while(!list.isEmpty())
{
currentNode = list.poll();
System.out.println(currentNode.value);
if(currentNode.left != null)
list.add(currentNode.left);
if(currentNode.right != null)
list.add(currentNode.right);
}
}
先序遍历特点:第一个值是根节点
中序遍历特点:根节点左边都是左子树,右边都是右子树
3.翻转句子中单词的顺序
定义两个指针,依次交换对应的字符串,即可
例如 str = “I am a student.”,array = {“I”,“am”, “a”, “student.”}
array[0]和array[3]交换,{“student.”, “am”, “a”, “I”}
array[1]和array[2]交换,{“student.”, “a”, “am”, “I”}
public static String reverseStringSequence(String str) {
if (Strings.isNullOrEmpty(str)) {
return str;
}
String[] seq = str.split(" ");
// 定义两个指针,一个从头开始,一个从尾开始,成对交换,当两个指针相遇时则停止
int start = 0;
int end = seq.length - 1;
while (start < end) {
String temp = seq[start];
seq[start] = seq[end];
seq[end] = temp;
start++;
end--;
}
return StringUtils.join(seq, " ");
}
public static void main(String[] args) {;
String result = reverseStringSequence("I am a student.");
System.out.println(result);
}
解法二
思路和上面一样,都是字符串反转,这里不以单词为单位,而是以字符为单位,所以需要进行两步反转
对每个单词进行反转得到"I ma a .tneduts"
反转整个字符串得到"student. a am I"
public static String reverseStringSequence(String str) {
if (Strings.isNullOrEmpty(str)) {
return str;
}
char[] seq = str.toCharArray();
int length = seq.length;
// 定义两个指针记录要反转单词的起始位置
int start = 0;
int end = 0;
// 这里一定要含有等于,因为要判断是否是最后一个单词,从而可以处理最后一个单词
while (end <= length) {
// 当已经遍历到字符串的最后一个字符,或者当前字符是空格时
// 则对空格前的单词进行反转,即"am"反转为"ma"
// 一定要把判断是否是结尾放在前面,否则seq[end]会报错,因为数组的有效索引是从0开始的
// 反转后修改单词的起始指针为空格的下一个字符
// 如果不符合条件,则移动指针继续判断下一个字符
if (end == length || seq[end] == ‘ ‘) {
reverse(seq, start, end - 1);
start = end + 1;
}
end++;
}
// 反转这个数组
reverse(seq, 0, length - 1);
return new String(seq);
}
private static void reverse(char[] seq, int start, int end) {
while (start < end) {
char temp = seq[start];
seq[start] = seq[end];
seq[end] = temp;
start++;
end--;
}
}
4.用栈模拟队列(或用队列模拟栈)
问题描述
用两个栈实现队列
用两个队列实现栈
问题分析
用两个栈实现队列
stackPush 用作 Push 操作,Push 是直接 push 进这个栈。
stackPop 用作 Pop 操作,若 stackPop 当前不为空,那么直接 pop ,若为空,那么将 stackPush 全部倒入 stackPop 中,然后 stackPop 再 pop 出一个元素
用两个队列实现栈
用两个队列 queue 队列 与 help 队列
Push 时直接 push 进 queue队列
Pop 时先检查queue队列是否为空,若为空,说明所要实现的栈中不存在元素。若不为空,则将 queue队列中的元素装入 help中,queue中只剩最后一个,那么这便是应该Pop的元素,将该元素pop。pop之后,交换两队列,原help队列作现queue,原queue作现help即可。
代码实现
package basic_class_03;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
public class MyStackAndQueueConvert {
//用两个栈实现队列
public static class TwoStackQueue{
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
public TwoStackQueue() {
this.stackPop = new Stack<>();
this.stackPush = new Stack<>();
}
public void push (int pushInt) {
stackPush.push(pushInt);
}
public int poll() {
if (stackPop.isEmpty() && stackPush.isEmpty()) {
throw new RuntimeException("Queue is empty!");
}else if (stackPop.isEmpty()) {
while (! stackPush.isEmpty()) {
stackPop.push(stackPush.pop());
}
}
return stackPop.pop();
}
public int peek() {
if (stackPop.isEmpty() && stackPush.isEmpty()) {
throw new RuntimeException("Queue is empty!");
}else if (stackPop.isEmpty()) {
while (! stackPush.isEmpty()) {
stackPop.push(stackPop.pop());
}
}
return stackPop.peek();
}
}
//用两个队列实现栈
public static class TwoQueuesStack{
private Queue<Integer> queue;
private Queue<Integer> help;
public TwoQueuesStack() {
queue = new LinkedList<>();
help = new LinkedList<>();
}
public void push(int pushInt) {
queue.add(pushInt);
}
public int peek() {
if (queue.isEmpty()) {
throw new RuntimeException("Stack is empty!");
}
while (queue.size() != 1) {
help.add(queue.poll());
}
int res = queue.poll();
help.add(res);
swap();
return res;
}
public int pop() {
if (queue.isEmpty()) {
throw new RuntimeException("Stack is empty!");
}
while (queue.size() != 1) {
help.add(queue.poll());
}
int res = queue.poll();
swap();
return res;
}
public void swap() {
Queue<Integer> temp = queue;
queue = help;
help = temp;
}
}
}
5.对10亿个数进行排序,限制内存为1G
采用分治的思路
3、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
方案:顺序读文件中,对于每个词x,取hash(x)%5000,然后按照该值存到5000个小文件(记为x0,x1,…x4999)中。这样每个文件大概是200k左右。
如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,直到分解得到的小文件的大小都不超过1M。
对每个小文件,统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结 点的最小堆),并把100个词及相应的频率存入文件,这样又得到了5000个文件。下一步就是把这5000个文件进行归并(类似与归并排序)的过程了。
6.去掉(或找出)两个数组中重复的数
排序和hash两种思路
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
因为数组中的元素都是0到n-1的,所以数组的值可以作为数组的下标,hash数组原始都为为,遍历原数组,取到的值作为index,判断在hash数组中这个 index数字对应的值,如果是0,还没有出现过,赋值为1,如果是1了,就说明出现过了,返回现在取到的numbers数组的值
public class Solution {
// Parameters:
// numbers: an array of integers
// length: the length of array numbers
// duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
// Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
// 这里要特别注意~返回任意重复的一个,赋值duplication[0]
// Return value: true if the input is valid, and there are some duplications in the array number
// otherwise false
public boolean duplicate(int numbers[],int length,int [] duplication) {
int hash[]=new int[length];
for(int i=0;i<length;i++){
if(hash[numbers[i]]==0){
hash[numbers[i]]++;
}
else{
duplication[0]=numbers[i];
return true;
}
}
return false;
}
}
public static int[] distinct(int[] arr){
int length=1;
boolean isExist=false;
for(int i=1;i<arr.length;i++){
for(int j=0;j<length;j++){
if(arr[i]==arr[j]){
isExist=true;
break;
}
}
if(!isExist){
arr[length]=arr[i];
length++;
}
isExist=false;
}
int[] newArr=new int[length];
System.arraycopy(arr, 0, newArr, 0, length);
return newArr;
}
7.将一颗二叉树转换成其镜像
package tree;
public class MirrorTree {
/**
* 将一颗二叉树转换成它的镜像
* @param args
*/
public static void convertmirror(TreeNode root){
if(root==null||(root.left==null&&root.right==null)) return;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
convertmirror(root.left);
convertmirror(root.right);
}
public static void printf(TreeNode root){
if(root==null) return;
System.out.print(root.value+" ");
printf(root.left);
printf(root.right);
}
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(3);
root.right = new TreeNode(2);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(4);
printf(root);
System.out.println();
convertmirror(root);
printf(root);
}
}
8.确定一个字符串中的括号是否匹配
1、将字符串的每个字符进行遍历
2、如果发现是左括号,那么将该字符压入到栈中
3、如果是右括号,先去存储好的栈顶找到相应的值
4、若栈为空返回false,若匹配,pop该左括号,若不匹配也返回false
5、最后看存储栈中的做括号是否都匹配上了,也就是栈最后为空,返回true,否则返回false
package Algro;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
public class Match {
static boolean isMatch(String s){
//定义左右括号的对应关系
Map<Character,Character> bracket = new HashMap<>();
bracket.put(‘)‘,‘(‘);
bracket.put(‘]‘,‘[‘);
bracket.put(‘}‘,‘{‘);
Stack stack = new Stack();
for(int i = 0; i < s.length(); i++){
Character temp = s.charAt(i);//先转换成字符
//是否为左括号
if(bracket.containsValue(temp)){
stack.push(temp);
//是否为右括号
}else if(bracket.containsKey(temp)){
if(stack.isEmpty()){
return false;
}
//若左右括号匹配
if(stack.peek() == bracket.get(temp)){
stack.pop();
}
else{
return false;
}
}
}
return stack.isEmpty()? true: false;
}
public static void main(String[] args) {
System.out.println(isMatch("(***)-[{-------}]")); //true
System.out.println(isMatch("(2+4)*a[5]")); //true
System.out.println(isMatch("({}[]]])")); //false
System.out.println(isMatch("())))")); //false
}
}
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
public class BracketMatching {
// pair以右括号为key, 左括号为值
private Map<Character, Character> pair = null;
public BracketMatching()
{
pair = new HashMap<Character, Character>();
pair.put(‘)‘, ‘(‘);
pair.put(‘}‘, ‘{‘);
pair.put(‘]‘, ‘[‘);
}
public boolean isMatch(String s)
{
Stack<Character> sc = new Stack<Character>();
for (int i = 0; i < s.length(); i++)
{
Character ch = s.charAt(i);
if (pair.containsValue(ch))// 如果是左括号,放入栈中
{
sc.push(ch);
} else if (pair.containsKey(ch)) // 如果是右括号
{
if (sc.empty()) // 栈为空,栈头没有字符与右括号匹配
{
return false;
}
// 栈不为空,栈头字符与右括号匹配
if (sc.peek() == pair.get(ch))
{
sc.pop();
} else //网上许多列子没有这里的else代码块,导致({}[]]])会被判断为true
{ // 栈不为空,栈头字符不与右括号匹配
return false;
}
}
}
return sc.empty() ? true : false;
}
public static void main(String[] args)
{
BracketMatching judger = new BracketMatching();
System.out.println(judger.isMatch("(***)-[{-------}]")); //true
System.out.println(judger.isMatch("(2+4)*a[5]")); //true
System.out.println(judger.isMatch("({}[]]])")); //false
System.out.println(judger.isMatch("())))")); //false
System.out.println(judger.isMatch("((()")); //false
System.out.println(judger.isMatch("(){[[]]}")); //true
}
}
9.给定一个开始词,一个结束词,一个字典,如何找到从开始词到结束词的最短单词接龙
考虑使用深度优先算法
给定的俩个字串和字典中的串是一张无向图,构造一棵图的树,广度遍历(按层)这棵树,每次改变开始串的一个字符,如果在字典中就入队列,并从字典中删除,防止重复。
int ladderLength(string start, string end, unordered_set<string> &dict) {
int count=1;
unordered_set<string> ret=dict;
queue<string> que;
que.push(start);
while(!que.empty()){
int size=que.size();
//构造树当前层循环
while(size--){
string s=que.front();
que.pop();
for(int i=0;i<s.length();i++){
string word=s;
for(char j=‘a‘;j<=‘z‘;j++){
if(s[i]==j)
continue;
s[i]=j;
if(s==end)
return count+1;
if(ret.count(s)>0){
que.push(s);
ret.erase(s);
}
}
s=word; //还原字符串
}
}
//遍历完一层 加 1
count++;
}
return 0;
}
10.如何查找两个二叉树的最近公共祖先
递归与非递归皆可实现
根据Wikipedia对LCA的定义:“在两个节点p和q之间定义的最低公共祖先是T中同时具有p和q作为后代的最低节点(在这里,我们允许一个节点作为其自身的后代)。”
解决
与找二叉搜索树的最近公共祖先类似,如果一个节点的左子树上有与p或q相等的节点且右子树上有与p或q相等的节点,说明此时该节点即为最近公共节点
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//递归出口
if(root == null || root == p || root ==q)
return root;
//去该节点的左子树上找
TreeNode left = lowestCommonAncestor(root.left, p, q);
//去该节点的右子树上找
TreeNode right = lowestCommonAncestor(root.right, p, q);
if(left == null){
//左子树上没有,说明在右子树上
return right;
}else if(right == null){
//右子树上没有,说明在左子树上
return left;
}
//左右均有,说明该节点即为最近公共祖先
return root;
}
原文地址:https://www.cnblogs.com/muzinan110/p/11105693.html