网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 |
如果右边没有就找找左边 |
一、数据结构
数据结构有两种结构 |
- 线性结构
- 常用的数据结构,特点是数据元素之间存在一对一的线性关系
- 有两种不同存储结构:顺序存储结构(线性表称为顺序表,存储元素是连续的)、链式存储结构(线性表称为链表,存储元素不一定连续,元素结点中存放数据元素以及相邻元素的地址信息)
- 常见线性结构有:数组、队列、链表和栈
- 非线性结构
- 二维数组,多维数组,广义表,树结构,图结构
1. 稀疏数组(了解时间换空间的思路)
何为稀疏数组 |
- 假设保存一个五子棋的棋盘状态,我们通过二维数组表示棋盘,0表示空白,1表示黑子,2表示白子,下图可以看出,二维数组中记录了很多没意义的0,这时避免记录没意义的数据,就可以使用稀疏数组
- 处理方法
- 记录数组一共几行几列,有多少不同值(一般记录在arr[0]中)
- 把具有不同值的元素的行列及值记录在一个小规模数组中,从而缩小程序规模
- 稀疏数组实例(可见一个二维数组作为稀疏数组 arr[0]也就是第0行数据,记录棋盘共多少行,多少列,有几个不同值,就是非0值。因为一共8个非0值,然后依次记录这8个值的行列和值)
代码及运行结果 |
- 运行结果分析(可见类似这种无用元素非常多的数值,非常浪费空间,此时我们完全可以使用时间换取空间的方法,将数组压缩成为稀疏数组,这样我们在传输数据时,传输的数据量就由11 * 11=121个元素变为了3*3=9个)
- 代码(很多代码都是遍历的代码,可以省略,而且稀疏数组可以直接替换为集合,更加方便快捷)
public class SparseArray {
public static void main(String[] args) {
//1. 先建立原数组,有很多重复数据
int arr[][] = new int[11][11];
arr[1][2]=1;
arr[2][3]=2;
int sum = 0;
System.out.println("==============原数组长这样==========");
for(int i = 0;i< arr.length;i++){
for(int j = 0;j<arr[i].length;j++){
System.out.print(arr[i][j]+" ");
//2. 记录非零元素个数
if(arr[i][j]!=0) sum++;
}
System.out.println();
}
System.out.println("================共有"+sum+"个非0元素====================");
//3.创建稀疏数组
int sparseArr[][] = new int[sum+1][3];
sparseArr[0][0] = arr.length;//第0行记录行、列、非0元素个数
sparseArr[0][1] = arr[0].length;
sparseArr[0][2] = sum;
int index = 1;
//4. 记录非0值
for(int i =0;i<arr.length;i++){
for (int j = 0;j<arr[i].length;j++){
if(arr[i][j]!=0) {
sparseArr[index][0] = i;
sparseArr[index][1] = j;
sparseArr[index][2] = arr[i][j];
index++;
}
}
}
//5. 遍历稀疏数组并根据稀疏数组还原原数组
System.out.println("===============稀疏数组结果===============");
int arr2[][]=new int[sparseArr[0][0]][sparseArr[0][1]];
for(int i = 0;i< sparseArr.length;i++){
System.out.println(sparseArr[i][0]+" "+sparseArr[i][1]+" "+sparseArr[i][2]+" ");
if(i!=0) arr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
//6. 遍历根据稀疏数组还原的数值,查看是否和原数组一致
System.out.println("==============还原后数组长这样==========");
for(int i = 0;i< arr2.length;i++){
for(int j = 0;j<arr2[i].length;j++){
System.out.print(arr2[i][j]+" ");
}
System.out.println();
}
}
}
2. 队列
何为队列 |
- 队列可以想象为一条只能同时通过一个人的走廊,那么有100个人想要从走廊一边走到另一边出去,那么就需要依次排队进入,先进去的就先从另一边出去,后进去的就后出
- 队列是一个有序列表,可用数组或链表实现,需要遵循先入先出原则,就是先进入队列中的程序、线程、数据,要先处理或执行,后进入的后处理
- 简单来说,就是我们从队列取数据,优先从头部取数据,也就是排在最前面,最先进队列的,而我们向队列添加数据,先从尾部添加,所以我们应该有两个指针,分别指向队列的头和尾,同时也要有一个属性,代表队列的容量
环形队列 |
- 因为普通队列有一个问题,就是每取一个数据,头指针向后移一位,那么一旦头指针移到队列末尾,或者尾指针移动到末尾,此时没法移动到第一个位置,重新添加数据或获取数据,也就是一个队列只能使用一次,而循环队列解决了这个问题,当头或尾指针到达队列末尾,会根据算法,移动到可以使用的地方
代码,实现思路看代码注释 |
- 实现结果(可见初始队列大小为3有3个元素1、2、3,头元素为1,当我们取出头元素1时,头元素变为2,当我们继续添加元素4时,根据环形队列特性,将前往第一个位置添加,但是我们遍历时,不能这样遍历出来,所以使用算法,达到后添加的出现在后面的效果)
- 代码,第一个代码块是队列数据结构整体实现,第二个代码块是测试类
//用一个类来表示循环队列数据结构,用数组实现版本
public class MyQueue {
private int maxSize;//表示队列最大容量,默认为10,可以存9个元素,因为我们思路,给rear一个预留位置,所以rear需要占一个位置无法存储元素
private int front;//指向队列头的位置,也就是第一个元素的位置,初始是0
private int rear;//指向队列最后一个元素的后一个位置,初始是0
//另外根据队列特性,front永远没法跑到rear的后面,所以遍历的时候,总是从front遍历到rear即可
private int[] arr;//存放数据的载体
/**
* 1.创建(初始化)队列
*/
public MyQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new int[this.maxSize];
this.front = this.rear = 0;
}
public MyQueue(){
this.maxSize = 10;
this.arr = new int[this.maxSize];
this.front = this.rear = 0;
}
/**
* 2.判断队列是否已满,已满返回true,未满返回false
*/
public boolean isFull(){
return (rear+1) % maxSize == front;//如果尾部元素下标取余队列大小的值,刚好是头指针位置,代表队列满
}
/**
* 3.判断队列是否为空,为空返回true
*/
public boolean isNull(){
return rear == front;
}
/**
* 4.判断队列有效元素的个数
*/
public int effectiveElementCount(){
return (rear+maxSize-front)%maxSize;//记住,判断一个线性结构的有效元素,(尾指针+容器大小-头指针)%容器大小即可求出
}
/**
* 5.添加数据到队列
*/
public boolean add(int param){
if(isFull()) return false;//队列已满,无法添加,返回false
arr[this.rear] = param;//因为rear始终指向尾元素的后一个位置,那么新加入元素时,直接在这个位置添加即可
this.rear=(this.rear+1) % maxSize;//然后让rear后移,代表新尾元素插入后的新的后一个位置
return true;
}
/**
* 6.从队列取数据
*/
public int get(){
if (isNull()) {//如果队列为空,抛出异常
throw new RuntimeException("队列为空!!!无法获取数据");
}else{//队列不为空,可以取出数据
int param = arr[this.front];//因为front指向当前头元素,先将值保存
this.front = (this.front+1)%maxSize;//然后让front指向下一个元素
return param;
}
}
/**
* 7.显示队列所有数据
*/
public void showQueue(){
if (isNull()) return;
for (int i = front;i!=rear;i=(i+1)%maxSize){
System.out.print(arr[i]+" ");
}
//遍历的两种方法,大家随意选择
// for (int i = front;i< front + effectiveElementCount();i++){
// System.out.print(arr[i%maxSize]+" ");
// }
System.out.println();
}
/**
* 8.显示队列头数据
*/
public int peekHead(){
if (isNull()) throw new RuntimeException("队列为空!!!没有头数据");
return arr[this.front];
}
}
public class Test {
public static void main(String[] args) {
MyQueue myQueue = new MyQueue(4);
myQueue.add(1);
myQueue.add(2);
myQueue.add(3);
System.out.println("队列元素为:");
myQueue.showQueue();
System.out.println("头元素为"+myQueue.peekHead());
System.out.println("是否满"+myQueue.isFull());
System.out.println("取出一个数据"+myQueue.get());
System.out.println("队列元素为:");
myQueue.showQueue();
System.out.println("头元素为"+myQueue.peekHead());
System.out.println("添加一个元素4");
myQueue.add(4);
System.out.println("队列元素为:");
myQueue.showQueue();
System.out.println("头元素为"+myQueue.peekHead());
if(!myQueue.add(5)) System.out.println("添加元素5,队列满!!!无法继续添加元素");
}
}
3. 单链表
何为单链表 |
- 链表虽然是顺序存储,但在内存中未必
- 根据上图分析,head头结点,是链表第一个元素,它记录了下一个元素的内存地址150,那么通过150这个内存地址,找到a1元素,a1元素记录了它下一个元素地址110,通过110这个地址又可以找到a2元素,a2记录180地址,找到a4。依次类推
- 链表有带头结点的,也有不带头结点的,根据实际需求使用相应的即可
- 带头结点单链表
逻辑结构
(这是逻辑结构,虽然图表示出来a1后面就是a2,a2后面就是a3,但是实际上在内存中,他们未必甚至很难是连续的,而是通过指针依次找到他们,详细看上一张图)
实现思路与代码 |
- 运行结果与思路(首先链表,整体是一个对象,而每个结点又是一个对象(每个结点可以存储任意类型元素数据,但是我们链表只存储结点对象),整个链表通过head表示头结点,last表示下一个结点,next是每个结点指向的下一个节点,没有下一个节点,则赋值为null)
- 代码(第一个代码块是结点,第二个是链表,第三个是测试类)
/**
* 链表节点
*/
public class ListNode {
private Object data;//数据
private ListNode next;//下一个节点,java没有指针,java将其封装成了引用,我们只需要引用这个对象,就可以拿到其内存地址
public ListNode(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public ListNode getNext() {
return next;
}
public void setNext(ListNode next) {
this.next = next;
}
}
/**
* 表示整个单链表
*/
public class SingleLinkList {
private ListNode head = null;//头结点,初始为null,记录整个链表第一个节点
private ListNode last = null;//记录下一个节点
public int size = 0;//记录元素个数
/**新增一个元素**/
public void add(Object element) {
if(head == null) {//如果头结点为null,表示没有头结点
head = new ListNode(element);//如果没有头结点,代表第一次添加元素,让头结点指向这个节点
last = head;//第一次添加,最后一个元素就是头结点
}else{
last.setNext(new ListNode(element));//如果不是第一次添加,让当前末尾节点的后继为新插入的元素,此时,末尾节点变成了新插入的元素,也就是last当前指向节点的后继
last = last.getNext();//所以我们让last指向新节点的后继,就完成了指向末尾节点的操作
}
size++;//添加完成,链表长度加1
}
/**判断指定元素是否存在,如果存在返回下标,不存在返回负1**/
public int indexOf(Object element) {
ListNode node = head;
int index = 0;
while(node != null){
if(node.getData().equals(element)){
return index;
}
index++;
node = node.getNext();
}
return -1;
}
/**删除指定元素**/
public void delete(Object element) {
ListNode node = head;
ListNode pre = null;//用来记录node的上一个节点,初始,头结点的上一个没有,是null
while(node != null){
if(node.getData().equals(element)){//如果找到了要删除的元素
if(pre != null) {//如果有前驱
if(node.getNext()==null) //如果是最后一个结点,没有后继,那么直接让前驱的next变成空
pre.setNext(null);
else //如果不是最后一个结点,直接让前驱的next,指向当前要删除节点的next,这样就正好跳过了要删除的结点,那么这个结点就会因为没有引用指向,变为垃圾,被java垃圾回收器回收
pre.setNext(node.getNext());
}else{//如果没有前驱,就说明是头结点,直接让头结点指向当前节点的next
head = node.getNext();
}
size--;
}
pre = node;//pre记录当前node节点
node = node.getNext();//node节点指向下一个节点,此时pre就是当前node的上一个节点
}
}
/**获取指定下标结点**/
private ListNode selectNodeByIndex(Integer index) {
if(index < 0 || index >= size){
System.out.println("下标无效");
return null;
}
ListNode node = head;
int i = 0;
while(i !=index){
node = node.getNext();
i++;
}
return node;
}
/**删除指定索引元素**/
public void deleteByIndex(Integer index) {
if(index < 0 || index >= size){
System.out.println("下标无效");
return;
}
delete(selectNodeByIndex(index).getData());//这里偷懒,直接用上面写好的delete方法了,可以优化的
}
/**修改指定索引的元素**/
public void update(Integer index, Object newElement) {
selectNodeByIndex(index).setData(newElement);
}
/**查询列表中是否存在指定值**/
public boolean selectByElement(Object element) {
if(indexOf(element)>=0) return true;
return false;
}
/**查询指定下标元素**/
public Object selectByIndex(Integer index) {
return selectNodeByIndex(index).getData();
}
public String toString() {
StringBuffer stringBuffer = new StringBuffer();
ListNode node = head;
stringBuffer.append("[");
while(node.getNext()!=null){
stringBuffer.append(node.getData()+" , ");
node = node.getNext();
}
stringBuffer.append(node.getData()+"]");
return stringBuffer.toString();
}
}
class Test {
public static void main(String[] args) {
Test arr[] = new Test[7];
for (int i =0 ;i<7;i++){
arr[i] = new Test();
}
System.out.println("=========================");
arr[0].add();
System.out.println("=========================");
arr[1].indexOf();
System.out.println("=========================");
arr[2].delete();
System.out.println("=========================");
arr[3].deleteByIndex();
System.out.println("=========================");
arr[4].update();
System.out.println("=========================");
arr[5].selectByElement();
System.out.println("=========================");
arr[6].selectByIndex();
}
private SingleLinkList singleLinkList = new SingleLinkList();
void add() {
singleLinkList.add(1);
singleLinkList.add("张三");
singleLinkList.add(3);
singleLinkList.add(4);
System.out.println("当前链表元素为:"+singleLinkList.toString()+"~~~~共有元素"+singleLinkList.size+"个");
}
void indexOf() {
add();
System.out.println("元素 1 的下标为:"+singleLinkList.indexOf(1));
System.out.println("元素 张三 的下标为:"+singleLinkList.indexOf("张三"));
System.out.println("元素 3 的下标为:"+singleLinkList.indexOf(3));
System.out.println("元素 4 的下标为:"+singleLinkList.indexOf(4));
}
void delete() {
add();
singleLinkList.delete(1);
System.out.println("删除1后的链表为:"+singleLinkList.toString());
singleLinkList.delete(3);
System.out.println("删除3后的链表为:"+singleLinkList.toString());
singleLinkList.delete(4);
System.out.println("删除4后的链表为:"+singleLinkList.toString());
}
void deleteByIndex() {
add();
singleLinkList.deleteByIndex(0);
System.out.println("删除下标为0元素后的链表为:"+singleLinkList.toString());
singleLinkList.deleteByIndex(1);
System.out.println("删除下标为1元素后的链表为:"+singleLinkList.toString());
singleLinkList.deleteByIndex(1);
System.out.println("删除下标为1元素后的链表为:"+singleLinkList.toString());
}
void update() {
add();
singleLinkList.update(0,"new 0");
System.out.println("修改下标为0元素数据为new 0 后链表:"+singleLinkList.toString());
singleLinkList.update(1,"new 1");
System.out.println("修改下标为1元素数据为new 1 后链表:"+singleLinkList.toString());
singleLinkList.update(2,"new 2");
System.out.println("修改下标为2元素数据为new 2 后链表:"+singleLinkList.toString());
singleLinkList.update(3,"new 3");
System.out.println("修改下标为3元素数据为new 3 后链表:"+singleLinkList.toString());
}
void selectByElement() {
add();
System.out.println("列表是否存在数据 张三:"+singleLinkList.selectByElement("张三"));
}
void selectByIndex() {
add();
System.out.println("下标为2的数据为:"+singleLinkList.selectByIndex(2));
}
}
面试题1:求单链表中有效节点个数
- 运行效果
- 代码
/**
* 获取单链表结点个数(如果有头结点,根据实际情况统计头结点,我们这里写的头结点就是链表第一个节点,所以需要统计)
* 因为需要传入参数头结点,所以我们链表中要提供获取头结点head的Get方法
*/
public int getLength(ListNode head){
if(head == null) return 0;//如果头结点为null,表示链表为空
int length = 1;//否则统计头结点
ListNode node = head.getNext();
while(node != null){
length++;
node = node.getNext();
}
return length;
}
面试题2:查找单链表中的倒数第k个节点
- 实现结果
- 代码
/**
* 查找单链表的倒数第k个节点
*/
public ListNode TheLastNumber(int k){
//1. 算出倒数第k个节点下标,如果链表没有size字段记录链表总长度,需要单独遍历一遍所有节点,获取总长度,
//其实上一道题就可以求出总长度
int index = size - k;
//2. 根据下标获取节点
if(selectNodeByIndex(index)==null) {
System.out.println("没有找到结点···省略了异常处理");
return null;
}else{
return selectNodeByIndex(index);
}
}
/**获取指定下标结点**/
private ListNode selectNodeByIndex(Integer index) {
if(index < 0 || index >= size){
System.out.println("下标无效");
return null;
}
ListNode node = head;//第三方节点,用来帮助我们遍历
int i = 0;
while(i !=index){//不断的循环,直到找到目标下标节点
node = node.getNext();
i++;
}
return node;
}
面试题3:单链表的反转
- 运行效果
- 代码
/**
* 反转单链表
* 1. 创建新的一个结点reverseHead,作为反转链表的头结点,但是,它相当于火车头,不拉任何东西,始终是一个辅助结点,不保存数据
* 2. 依次遍历原链表
* 3. 记录每一个结点和它的下一个节点
* 4. 当我们取出第一个节点时,我们让它的后继指向reverseHead的后继,然后让reverseHead的后继指向它,就好像它插入到了原来reverseHead和reverseHead的next的中间
* 就像这样 reverseHead->next node1作为reverseHead的后继插入------> reverseHead->node1->next
* 5. 当我们取出第二节点时,我们让它的后继指向reverseHead的后继,就是第一个节点,然后让reverseHead的后继指向他,这样它就插入到了reverseHead和第一个节点中间
* 就像这样 reverseHead->node1->next --------> reverseHead->node2->node1->next
* 6. 最后让头结点指向reverseHead.next,以此达到反转目的
*/
public void reverseLinkList(){
//1.创建反转头结点,
ListNode reverseHead = new ListNode(0);
//2.依次遍历原链表的辅助结点
ListNode node = head;//用来遍历链表的辅助节点
head = null;//这里头结点已经记录在node中,它可以变为null,最后我们重新让它作为反转后链表的头结点
//3. 用来记录node的下一个节点,否则会丢失
ListNode next = null;
//遍历原链表
while(node != null){
//3.记录下一个节点
next = node.getNext();
//4.后继指向reverseHead的后继------> node->next
node.setNext(reverseHead.getNext());
//4. reverseHead的后继指向node-----> reverseHead->node->next
reverseHead.setNext(node);
// 然后继续遍历下一个
node = next;
}
//让头结点指向反转后的链表头
head = reverseHead.getNext();
}
面试题4:从尾到头打印单链表(方式一:反向遍历,方式二:Stack栈)
- 分析及运行效果
- 方式一反向遍历:先将单链表反转(上一道题的方法),然后遍历,会破坏链表结果,浪费空间,不推荐
- 利用数据结构,栈,将各个节点压入栈中,因为栈的特性是先进后出,就可以方便实现逆序打印
- 代码
/**
* 从尾到头打印单链表
*/
public void reversePrint(){
ListNode node = head;//遍历链表的辅助变量
Stack<ListNode> stack = new Stack<>();//创建一个栈
while(node != null){
stack.add(node);//依次将结点压入栈
node = node.getNext();
}
while(stack.size()!=0){
System.out.print(stack.pop().getData()+" ");//循环出栈
}
}
面试题5:合并两个有序的单链表,合并之后的链表依然有序
面试题6:Josephu约瑟夫问题,使用单向环形链表
单向环形链表和单链表不同的地方就是,最后一个结点指向不是null了,而是头结点
- Josephu问题
- 设编号为1,2,…n的n个人围坐在一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,知道所有人出列为止,由此产生一个出队编号的序列
- 思路,利用不带头结点的循环链表处理Josephu问题,先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,记到m时,对应节点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束
- 代码
public class Josephu {
public static void main(String[] args) {
CircularLinkedList circularLinkedList = new CircularLinkedList();
circularLinkedList.add(1,2,3,4,5,6,7,8,9);
circularLinkedList.showPrint();
System.out.println("从第一个结点报数,报的3的结点出列:");
circularLinkedList.josephu(1,3);
}
}
//结点
class JosephuNode{
private int data;//结点数据
private JosephuNode next;//下一个结点
public JosephuNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public JosephuNode getNext() {
return next;
}
public void setData(int data) {
this.data = data;
}
public void setNext(JosephuNode next) {
this.next = next;
}
}
//环形单链表
class CircularLinkedList{
private JosephuNode first = null;//第一个节点
private int size = 0;//元素个数
/**
* 一次添加所有结点,构建循环链表
* @param nums 不定长参数,接收多个int值,会转换为数组
*/
public void add(int ...nums){
if(nums.length<=1) {
System.out.println("结点必须多于1个!!!");
}
size = nums.length;//记录链表元素个数
first = new JosephuNode(nums[0]);//将第一个数据结点,赋值给first
first.setNext(first);//构成环
JosephuNode node = first;//辅助结点,帮助构建循环链表,始终指向最后一个结点
for(int i = 1;i<nums.length;i++){//遍历剩下的结点
JosephuNode josephuNode = new JosephuNode(nums[i]);
node.setNext(josephuNode);
josephuNode.setNext(first);
node = josephuNode;
}
}
/**
* 遍历
*/
public void showPrint(){
//是否为空
if(first==null){
System.out.println("链表为空!!!");
return;
}
//辅助指针,帮助遍历
JosephuNode node = first;
System.out.println("循环链表值为:");
while(node.getNext()!=first){
System.out.print(node.getData()+" ");
node = node.getNext();
}
System.out.println(node.getData());
}
/**
* 约瑟夫解题,传入k,m
* 从k开始数,数m个数,然后将相应结点删除,向后继续数m个数
*/
public void josephu(int k , int m){
if(k > size) {
System.out.println("一共就"+size+"个节点,没有第"+k+"个值");
return;
}
//找出第k个节点,保存到node中
JosephuNode node = first;
for (int i = 1;i<k;i++){
node = node.getNext();
}
//开始遍历
while(size != 0){
for (int i = 2;i < m;i++){
node = node.getNext();
}
System.out.print(node.getNext().getData()+" ");
node.setNext(node.getNext().getNext());
node = node.getNext();
size--;
}
}
}
4. 双链表
双链表 |
- 它每个结点都有3个属性,前驱(记录前一个结点),数据,后继(记录后一个结点)
- 头结点前驱为null,尾结点后继为null
- 插入数据都是在两端插入,可以插入到前面,也可以插入到后面
- 如果头结点前驱指向为尾结点,尾结点后继指向头结点,此时链表变为循环链表(日后介绍)
运行效果及代码 |
运行效果2. 代码(第一个代码块是结点实体类,第二个代码块是双链表实现类,第三代码块是测试代码)
public class DoubleListNode {
private Object data;//数据
private DoubleListNode pre;//前驱,记录自己前一个结点
private DoubleListNode next;//下一个节点,java没有指针,java将其封装成了引用,我们只需要引用这个对象,就可以拿到其内存地址
public DoubleListNode(Object data) {
this.data = data;
}
public DoubleListNode(DoubleListNode pre, Object data, DoubleListNode next) {
this.pre = pre;
this.data = data;
this.next = next;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public DoubleListNode getPre() {
return pre;
}
public void setPre(DoubleListNode pre) {
this.pre = pre;
}
public DoubleListNode getNext() {
return next;
}
public void setNext(DoubleListNode next) {
this.next = next;
}
}
public class DoubleLinkList {
//初始化头结点和尾结点,因为双链表支持从头插入,和从尾插入
private DoubleListNode head;//头结点,初始为null
private DoubleListNode last;//记录末尾结点
public int size = 0;//记录元素个数
/**双链表特有功能,从头插入和从尾插入**/
public void linkFirst(Object element){//插入元素到头部
final DoubleListNode first = head;//保存当前头结点,我们称为插入之前的头结点
final DoubleListNode newNode = new DoubleListNode(null,element,first);//生成新结点,因为它插入当前头结点前面成为新头结点,所以前驱为null,后继为插入之前的头结点
head = newNode;//此时头结点已经变为新结点了
if(first == null){//判断插入之前是否存在头结点,如果不存在让尾结点直接指向刚插入元素,因为只有一个元素,即是头也是尾
last = newNode;
}else{//如果存在头结点,让插入之前的头结点,前缀变为刚插入的结点,因为它不是头结点了
first.setPre(newNode);
}
size++;
}
public void linkLast(Object element){//插入元素到尾部
final DoubleListNode l = last;//保存当前尾结点,我们称为插入之前的尾结点
final DoubleListNode newNode = new DoubleListNode(l,element,null);//生成新结点,因为它插入当前尾结点后面成为新尾结点,所以前驱为刚才的尾结点,后继为null
last = newNode;//此时尾结点已经变为新结点了
if(l == null){//判断插入之前是否存在尾结点,如果不存在让头结点直接指向刚插入元素,因为只有一个元素,即是头也是尾
head = newNode;
}else{//如果存在尾结点,让插入之前的尾结点,后继变为刚插入的结点,因为它不是尾结点了
l.setNext(newNode);
}
size++;
}
/**新增一个元素**/
public void add(Object element) {
linkLast(element);//默认从后往前插入
}
/**获取指定下标结点**/
private DoubleListNode selectNodeByIndex(Integer index) {
if(index < 0 || index >= size){
System.out.println("下标无效");
return null;
}
DoubleListNode node = head;
int i = 0;
while(i != index){
node = node.getNext();
i++;
}
return node;
}
/**获取指定元素结点**/
private DoubleListNode selectNodeByElement(Object element) {
DoubleListNode node = head;
while(node != null){
if(node.getData().equals(element)){
return node;
}
node = node.getNext();
}
return null;
}
/**判断指定元素是否存在,如果存在返回下标,不存在返回负1**/
public int indexOf(Object element) {
DoubleListNode node = head;
int index = 0;
while(node != null){
if(node.getData().equals(element)){
return index;
}
index++;
node = node.getNext();
}
return -1;
}
/**删除指定元素**/
public void delete(Object element) {
DoubleListNode node = selectNodeByElement(element);//找到要删除的结点
if(node == null) {
System.out.println("要删除的结点不存在");
return ;
}
if(node.getPre()==null)//如果没有前驱,那么这个元素是头结点
head = node.getNext();//让头结点变为它的后一个结点
else
node.getPre().setNext(node.getNext());//让要删除结点的前一个结点的后继,变成要删除结点的后一个结点
if(node.getNext()==null)//如果没有后继,那么这个元素是尾结点
last = node.getPre();//让尾结点变为它的前一个结点
else
node.getNext().setPre(node.getPre());//让要删除结点的后一个结点的前驱,变成要删除结点的前一个结点
size--;
}
/**删除指定索引元素**/
public void deleteByIndex(Integer index) {
if(index < 0 || index >= size){
System.out.println("下标无效");
return;
}
delete(selectNodeByIndex(index).getData());
}
/**修改指定索引的元素**/
public void update(Integer index, Object newElement) {
selectNodeByIndex(index).setData(newElement);
}
/**查询列表中是否存在指定值**/
public boolean selectByElement(Object element) {
if(indexOf(element)>=0) return true;
return false;
}
/**查询指定下标元素**/
public Object selectByIndex(Integer index) {
return selectNodeByIndex(index).getData();
}
@Override
public String toString() {
StringBuffer stringBuffer = new StringBuffer();
DoubleListNode node = head;
stringBuffer.append("[");
while(node.getNext()!=null){
stringBuffer.append(node.getData()+" , ");
node = node.getNext();
}
stringBuffer.append(node.getData()+"]");
return stringBuffer.toString();
}
}
class Test {
public static void main(String[] args) {
Test arr[] = new Test[7];
for (int i =0 ;i<7;i++){
arr[i] = new Test();
}
System.out.println("=========================");
arr[0].add();
System.out.println("=========================");
arr[1].indexOf();
System.out.println("=========================");
arr[2].delete();
System.out.println("=========================");
arr[3].deleteByIndex();
System.out.println("=========================");
arr[4].update();
System.out.println("=========================");
arr[5].selectByElement();
System.out.println("=========================");
arr[6].selectByIndex();
}
private DoubleLinkList doubleLinkList = new DoubleLinkList();
void add() {
doubleLinkList.add(1);
doubleLinkList.add("张三");
doubleLinkList.add(3);
doubleLinkList.add(4);
System.out.println("当前链表元素为:"+doubleLinkList.toString()+"~~~~共有元素"+doubleLinkList.size+"个");
}
void indexOf() {
add();
System.out.println("元素 1 的下标为:"+doubleLinkList.indexOf(1));
System.out.println("元素 张三 的下标为:"+doubleLinkList.indexOf("张三"));
System.out.println("元素 3 的下标为:"+doubleLinkList.indexOf(3));
System.out.println("元素 4 的下标为:"+doubleLinkList.indexOf(4));
}
void delete() {
add();
doubleLinkList.delete(1);
System.out.println("删除1后的链表为:"+doubleLinkList.toString());
doubleLinkList.delete(3);
System.out.println("删除3后的链表为:"+doubleLinkList.toString());
doubleLinkList.delete(4);
System.out.println("删除4后的链表为:"+doubleLinkList.toString());
}
void deleteByIndex() {
add();
doubleLinkList.deleteByIndex(0);
System.out.println("删除下标为0元素后的链表为:"+doubleLinkList.toString());
doubleLinkList.deleteByIndex(1);
System.out.println("删除下标为1元素后的链表为:"+doubleLinkList.toString());
doubleLinkList.deleteByIndex(1);
System.out.println("删除下标为1元素后的链表为:"+doubleLinkList.toString());
}
void update() {
add();
doubleLinkList.update(0,"new 0");
System.out.println("修改下标为0元素数据为new 0 后链表:"+doubleLinkList.toString());
doubleLinkList.update(1,"new 1");
System.out.println("修改下标为1元素数据为new 1 后链表:"+doubleLinkList.toString());
doubleLinkList.update(2,"new 2");
System.out.println("修改下标为2元素数据为new 2 后链表:"+doubleLinkList.toString());
doubleLinkList.update(3,"new 3");
System.out.println("修改下标为3元素数据为new 3 后链表:"+doubleLinkList.toString());
}
void selectByElement() {
add();
System.out.println("列表是否存在数据 张三:"+doubleLinkList.selectByElement("张三"));
}
void selectByIndex() {
add();
System.out.println("下标为2的数据为:"+doubleLinkList.selectByIndex(2));
}
}
五、栈
- 根据例子体会栈的作用(一个表达式,它是一个字符串,我们需要分析数字,运算符,然后确定运算顺序。)
- 什么是栈
- 栈的英文为(stack)
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为
栈顶(Top)
,另一端为固定的一端,称为栈底(Bottom)
。- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
- 入栈和出栈
实现思路,代码及运行结果 |
- 实现思路—数组
- 通过数组模拟栈,我们需要top变量,指向栈顶,入栈时,top++,出栈时top–,
- 栈的大小也需要一个变量来规定
- 运行结果
- 代码
public class ArrayStackDome {
public static void main(String[] args) {
ArrayStack arrayStack = new ArrayStack(5);
arrayStack.push(1);
arrayStack.push(2);
arrayStack.push(3);
arrayStack.push(4);
arrayStack.push(5);
arrayStack.showPrint();
for(int i = 0;i<6;i++){
System.out.println("=========执行出栈操作=============");
arrayStack.pop();
arrayStack.showPrint();
}
}
}
class ArrayStack{
private int[] stack;//模拟栈的容器
private int top = -1;//栈顶,初始是-1
private int maxSize;//栈的大小
public ArrayStack(int maxSize){
this.maxSize = maxSize;
stack = new int[maxSize];
}
/**
* 判断栈是否已满
*/
private boolean isFull(){
if(top >= maxSize -1) return false;
return true;
}
/**
* 判断栈是否为空
*/
public boolean isEmpty(){
return top == -1;
}
/**
* 入栈
*/
public void push(int element){
if(!isFull()) {
System.out.println("栈已满!!!");
return;
}
top++;
stack[top] = element;
}
/**
* 出栈
*/
public int pop(){
if(isEmpty()) {
throw new RuntimeException("栈为空,无法执行出栈操作!!!");
}
//将栈顶数据返回,然后top--
return stack[top--];
}
/**
* 遍历栈中元素
*/
public void showPrint(){
if( isEmpty()) {
System.out.println("栈已空!!!");
return;
}
System.out.println("栈中元素从栈顶到栈低依次为;");
for(int i = top;i>=0;i--){
System.out.print(stack[i]+" ");
}
System.out.println();
}
}
面试题1:使用栈完成计算一位数加减乘除表达式的结果
- 思路分析(两个栈,一个存表达式的数字,一个存表达式的运算符,数字直接入数字栈,每个运算符
入栈前
,都进行判断,如果栈中没有运算符直接入栈,如果不是判断优先级,优先级比栈顶运算符大,直接入栈,比栈顶小,那么弹出当前栈顶的运算符(不是现在要入栈的运算符)
,再弹出数字栈中的两个数,进行运算,运算结果入栈到数字栈,然后当前运算符入栈,依次类推)- 代码效果
- 代码(为了大家理解,加了很多输出语句,删掉其实没有多少代码)
public class ArrayStackDome {
public static void main(String[] args) {
String str = new String("7*2*2-5+1-5+3-4");
System.out.println("计算表达式:"+str);
char[] chars = str.toCharArray();//将字符串转换为字符数组
Map<Character, Integer> level = new HashMap<>();//代表运算符优先级
level.put('*',10);
level.put('/',10);
level.put('%',10);
level.put('+',9);
level.put('-',9);
ArrayStack numStack = new ArrayStack(15);//用来保存数字
ArrayStack operStack = new ArrayStack(15);//用来保存运算符
for (char c:chars) {//依次遍历表达式
if(level.get(c)!=null){//如果是运算符
if(operStack.isEmpty()){//判断栈是否空,如果为空直接入栈
System.out.println("符号栈为空=====入栈"+c);
operStack.push(c);
}else{//如果栈不为空,需要判断当前运算符与栈顶运算符谁优先级更高
char pop = (char)operStack.pop();//拿到栈顶运算符
System.out.print("弹出"+pop+"比较"+c+" ============");
if(level.get(c)<=level.get(pop)){//如果当前运算符优先级小于等于栈顶运算符,那么取两个数与栈顶运算符运算
System.out.println("当前运算符优先级<=栈顶运算符");
switch (pop){
case '+':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+a+"+"+b+"入栈"+(a+b));
numStack.push(a+b);break;
}
case '-':{//减法或除法,应该是先入栈的数在前面
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+b+"-"+a+"入栈"+(b-a));
numStack.push(b-a);break;
}
case '*':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+a+"*"+b+"入栈"+(a*b));
numStack.push(a*b);break;
}
case '/':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+b+"/"+a+"入栈"+(b/a));
numStack.push(b/a);break;
}
}
System.out.println(" 符号栈入栈"+c);
operStack.push(c);//运算完成后将当前运算符入栈
}else{//如果运算符优先级大于栈顶,直接入栈
System.out.println("===========运算符"+c+"优先级没有栈顶"+pop+"高,两个哥们入栈");
operStack.push(pop);//刚弹出了栈顶进行比较,需要重新压栈
operStack.push(c);//将运算符压栈
}
}
}else{//如果不是运算符,那么就是数字,压入数字栈
Integer integer = Integer.valueOf(c + "");
numStack.push(integer);
System.out.println("数字栈入栈======"+integer);
}
}
//处理最后留在栈中的
while(!operStack.isEmpty()){//如果符号栈为空,证明算完了
char pop = (char)operStack.pop();//拿到栈顶运算符
switch (pop){
case '+':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+a+"+"+b+"入栈"+(a+b));
numStack.push(a+b);break;
}
case '-':{//减法或除法,应该是先入栈的数在前面
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+b+"-"+a+"入栈"+(b-a));
numStack.push(b-a);break;
}
case '*':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+a+"*"+b+"入栈"+(a*b));
numStack.push(a*b);break;
}
case '/':{
int a = numStack.pop();
int b = numStack.pop();
System.out.print("数字栈弹出"+a+"和"+b+"计算"+b+"/"+a+"入栈"+(b/a));
numStack.push(b/a);break;
}
}
}
System.out.println();
numStack.showPrint();
System.out.println("弹出最终运算结果:"+numStack.pop());
}
}
面试题2:使用栈完成计算多位数加减乘除的结果
- 分析
- 我们需要解决字符串一位一位读入的问题,比如70+1的读入是,7、0、+、1
- 我们需要将70作为一个整体数字,压栈
- 运行效果
- 修改的代码部分
int num = 0;//用来记录多位数
for (int i = 0 ;i<chars.length;i++) {
char c = chars[i];
if(level.get(c)!=null){//如果是运算符
if(operStack.isEmpty()){//判断栈是否空,如果为空直接入栈
operStack.push(c);
}else{//如果栈不为空,需要判断当前运算符与栈顶运算符谁优先级更高
char pop = (char)operStack.pop();//拿到栈顶运算符
if(level.get(c)<=level.get(pop)){//如果当前运算符优先级小于等于栈顶运算符,那么取两个数与栈顶运算符运算
switch (pop){
case '+':{
int a = numStack.pop();
int b = numStack.pop();
numStack.push(a+b);break;
}
case '-':{//减法或除法,应该是先入栈的数在前面
int a = numStack.pop();
int b = numStack.pop();
numStack.push(b-a);break;
}
case '*':{
int a = numStack.pop();
int b = numStack.pop();
numStack.push(a*b);break;
}
case '/':{
int a = numStack.pop();
int b = numStack.pop();
numStack.push(b/a);break;
}
}
operStack.push(c);//运算完成后将当前运算符入栈
}else{//如果运算符优先级大于栈顶,直接入栈
operStack.push(pop);//刚弹出了栈顶进行比较,需要重新压栈
operStack.push(c);//将运算符压栈
}
}
}else{//如果不是运算符,那么就是数字,此时不能直接压入数字栈,而是判断它后面是不是还是数字
Integer integer = Integer.valueOf(c + "");
num=num*10+integer;
if(i != chars.length-1){//如果不是最后一位字符
if(level.get(chars[i+1])==null)//如果它后面还是数字,那么不进行压栈
continue;
}
numStack.push(num);
}
num = 0;
}
面试题3:前缀(波兰表达式)、中缀、后缀表达式(逆波兰表达式)
- 前缀表达式(波兰表达式)
- 前缀表达式的运算符位于操作数之前
- 举例:(3+4) * 5 - 6 对应前缀表达式为- * + 3 4 5 6
- 中缀表达式
- 就是我们常见的运算表达式(3+4)*5-6
- 是我们最为熟悉的表达式,但是对计算机来说却不好操作(我们前两道题就能看出此问题),因此,计算机计算结果时,会将中缀表达式转成其它表达式来操作(一般转变成后缀表达式)
- 后缀表达式(逆波兰表达式)
- 后缀表达式与前缀表达式相似,只是运算符位于
操作数
之后(可以理解为每个运算符会匹配前面两个数字运算,运算结果重新填回表达式,例如a+(b-c) 首先+运算符,对应a和(b-c),所以为a (b-c) +,而-运算符对应b和c,所以为 b c - ,那么最后将结果拼接为:a b c - +)- 例如:(3+4)*5-6 对应的后缀表达式就是3 4 + 5 * 6 -
- 再比如a+b对应a b +、a+(b-c)对应 a b c - +、a+(b-c)*d对应a b c - d * +
- a+d*(b-c)对应a d b c - * +
- a = 1+3 对应 a 1 3 + =
后缀表达式运行结果及代码 |
- 难点分析
- 中缀表达式转换为后缀表达式
- 运行结果
- 代码
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
public class Test {
public static void main(String[] args) {
//完成将一个中缀表达式转成后缀表达式的功能
//说明
//1. 1+((2+3)×4)-5 => 转成 1 2 3 + 4 × + 5 –
//2. 因为直接对str 进行操作,不方便,因此 先将 "1+((2+3)×4)-5" =》 中缀的表达式对应的List
// 即 "1+((2+3)×4)-5" => ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
//3. 将得到的中缀表达式对应的List => 后缀表达式对应的List
// 即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
String expression = "1+((2+3)*4)-5";//注意表达式
//将中缀表达式字符串转换为list,方便操作
List<String> infixExpressionList = toInfixExpressionList(expression);
System.out.println("中缀表达式对应的List=" + infixExpressionList); // ArrayList [1,+,(,(,2,+,3,),*,4,),-,5]
//将list转换为后缀表达式
List<String> suffixExpreesionList = parseSuffixExpreesionList(infixExpressionList);
System.out.println("后缀表达式对应的List" + suffixExpreesionList); //ArrayList [1,2,3,+,4,*,+,5,–]
//计算后缀表达式的值
System.out.printf("expression=%d", calculate(suffixExpreesionList)); // ?
}
//即 ArrayList [1,+,(,(,2,+,3,),*,4,),-,5] =》 ArrayList [1,2,3,+,4,*,+,5,–]
//方法:将得到的中缀表达式对应的List => 后缀表达式对应的List
public static List<String> parseSuffixExpreesionList(List<String> ls) {
//定义两个栈
Stack<String> s1 = new Stack<String>(); // 符号栈
//说明:因为s2 这个栈,在整个转换过程中,没有pop操作,而且后面我们还需要逆序输出
//因此比较麻烦,这里我们就不用 Stack<String> 直接使用 List<String> s2
//Stack<String> s2 = new Stack<String>(); // 储存中间结果的栈s2
List<String> s2 = new ArrayList<String>(); // 储存中间结果的Lists2
//遍历ls
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; //注意因为是存放到List, 因此按顺序输出就是对应的后缀表达式对应的List
}
//方法:将 中缀表达式转成对应的List
// s="1+((2+3)×4)-5";
public static List<String> toInfixExpressionList(String s) {
//定义一个List,存放中缀表达式 对应的内容
List<String> ls = 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) {
ls.add("" + c);
i++; //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++;
}
ls.add(str);
}
}while(i < s.length());
return ls;//返回
}
//完成对逆波兰表达式的运算
/*
* 1)从左至右扫描,将3和4压入堆栈;
2)遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
3)将5入栈;
4)接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
5)将6入栈;
6)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
*/
public static int calculate(List<String> ls) {
// 创建给栈, 只需要一个栈即可
Stack<String> stack = new Stack<String>();
// 遍历 ls
for (String item : ls) {
// 这里使用正则表达式来取出数
if (item.matches("\\d+")) { // 匹配的是多位数
// 入栈
stack.push(item);
} else {
// pop出两个数,并运算, 再入栈
int num2 = Integer.parseInt(stack.pop());
int num1 = Integer.parseInt(stack.pop());
int res = 0;
if (item.equals("+")) {
res = num1 + num2;
} else if (item.equals("-")) {
res = num1 - num2;
} else if (item.equals("*")) {
res = num1 * num2;
} else if (item.equals("/")) {
res = num1 / num2;
} else {
throw new RuntimeException("运算符有误");
}
//把res 入栈
stack.push("" + res);
}
}
//最后留在stack中的数据是运算结果
return Integer.parseInt(stack.pop());
}
}
//编写一个类 Operation 可以返回一个运算符 对应的优先级
class Operation {
private static int ADD = 1;
private static int SUB = 1;
private static int MUL = 2;
private static int DIV = 2;
//写一个方法,返回对应的优先级数字
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:
System.out.println("不存在该运算符" + operation);
break;
}
return result;
}
}
再向大家提供一个做了小数点和空格等处理的版本 |
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 运算符
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;
/**
* 括号
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}
/**
* 判断是不是数字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判断是不是运算符
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
/**
* 匹配运算等级
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一个运算符的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出结果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 运算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
六、哈希表(散列表)
- 根据键值对(key-value)直接进行访问的数据结构,通过key隐射value,让我们可以快速的通过key拿到对应value值。这个映射函数叫做散列函数,存放记录的数组叫做散列表
- 常见实现哈希表的方式:数组+链表、数组+二叉树
- 一般我们自己实现哈希表,都是为了一些特殊场景,这些场景使用redis等缓存产品有些大材小用
- 运行效果
- 代码
public class Test {
public static void main(String[] args) {
EmpHashTable empHashTable = new EmpHashTable(3);
empHashTable.add(new Emp(1,"张三"));
empHashTable.add(new Emp(2,"李四"));
empHashTable.add(new Emp(3,"王五"));
empHashTable.add(new Emp(4,"六六"));
empHashTable.add(new Emp(5,"七七"));
empHashTable.add(new Emp(6,"巴巴"));
empHashTable.add(new Emp(7,"救救"));
empHashTable.add(new Emp(8,"试试"));
empHashTable.print();
empHashTable.findById(3);
}
}
/**
* 雇员类结点,是链表节点
*/
class Emp{
private Integer id;
private String name;
private Emp next;
public Emp(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Emp getNext() {
return next;
}
public void setNext(Emp next) {
this.next = next;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
/**
* 雇员链表
*/
class EmpLinkedList{
private Emp head = null;
/**
* 添加一个雇员
* @param emp
*/
public void add(Emp emp){
if(head == null){//如果是第一次添加,直接让头结点指向它
head = emp;
}else{//如果不是,遍历链表,将其添加到链表最后面
Emp node = head;
while(node.getNext()!=null){
node = node.getNext();
}
node.setNext(emp);
}
}
/**
* 遍历
*/
public void print(){
if(head == null) {
System.out.println("链表为空!!!");
return;
}
Emp node = head;
while(node!=null){
System.out.print(node.toString()+"------");
node = node.getNext();
}
}
/**
* 根据id查找指定雇员
* 找到了返回对象,没找到返回null
*/
public Emp findById(Integer id){
if(head == null){
System.out.println("链表为空");
return null;
}
Emp node = head;
while(node!=null){
if(node.getId()==id) return node;
node = node.getNext();
}
return null;
}
}
/**
* 数组加链表实现的哈希表
*/
class EmpHashTable{
private EmpLinkedList[] empLinkedListArray;//数组+链表实现
private Integer size;//哈希表大小,就是有多少条链表
//初始化哈希表大小,有多少链表,并初始化链表
public EmpHashTable(int size){
this.size = size;
empLinkedListArray = new EmpLinkedList[size];
for(int i = 0 ;i<size;i++){
empLinkedListArray[i] = new EmpLinkedList();
}
}
/**
* 散列函数,决定一条数据保存到哪里,获取一条数据从哪里获取。取模法实现
*/
public int hashFun(int id){
return id % size;
}
/**
* 添加一个雇员到哈希表
* @param emp
*/
public void add(Emp emp){
int i = hashFun(emp.getId());//根据散列函数,获取本雇员,应该添加到哪个链表中
empLinkedListArray[i].add(emp);
}
/**
* 遍历哈希表
*/
public void print(){
for(int i = 0;i<size;i++){
System.out.print("key:"+i+" value{");
empLinkedListArray[i].print();
System.out.println("}");
}
}
/**
* 根据id查找雇员
* 找到了返回对象,没找到返回null
*/
public Emp findById(Integer id){
int i = hashFun(id);
Emp emp = empLinkedListArray[i].findById(id);
if(emp == null){
System.out.println("没找到!!!");
return null;
}
System.out.println("查找id为"+id+"的节点,找到的节点为"+emp.toString());
return emp;
}
}