java之数据结构与算法

Posted muzinan110

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java之数据结构与算法相关的知识,希望对你有一定的参考价值。

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;

以上是关于java之数据结构与算法的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法(Java)之查找

数据结构与算法(Java)之稀疏数组与队列

数据结构与算法合集

java数据结构与算法之反转单链表

java数据结构与算法之栈(Stack)设计与实现

数据结构与算法(Java)之递归实例