视频链接:https://www.bilibili.com/video/BV1HQ4y1d7th
视频范围P1 - P41
目录描述
本栏主要将数据结构和算法分为十六类:
- 数据结构和算法概述
- 栈
- 链表
- 稀疏数组和队列
- 递归
- 排序算法
- 查询算法
- 哈希表
- 树
- 树结构应用
- 多路查找树
- 图
- 常用10种算法
1.数据结构和算法概述
- 数据结构是计算机存储、组织数据的方式,是指相互之间存在一种或多种特定关系的数据元素的集合
- 程序 = 数据结构 + 算法
- 数据结构和算法相辅相成
数据结构分为:线性结构、非线性结构
结构 | 性质 | 代表 |
---|---|---|
线性结构 | 数据元素之间存在一对一的线性关系 | 数组、队列、链表、栈 |
非线性结构 | 二维数组、多维数组、广义表、树结构、图结构 |
线性结构有两种不同的存储结构:顺序存储结构(数组)和链式存储结构(链表)
存储结构 | 特点 |
---|---|
顺序存储结构(数组) | 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的 |
链式存储结构(链表) | 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素结点存放数据元素以及相邻元素的地址信息 |
2.栈
- 栈是限制插入和删除只能在一个位置上进行的线性表。
- 栈顶(top):允许插入和删除的一端位于表的末端
- 栈底(bottom):不允许插入和删除的另一端
- 对栈的基本操作有PUSH(压栈)和POP(出栈/弹栈)
- PUSH(压栈):相当于表的插入操作(想栈顶插入一个元素)
- POP(出栈/弹栈):删除操作(删除一个栈顶元素)
- 栈是一种后进先出(LIFO)的数据结构,最先被删除的是最近压栈的元素
- 栈实现:由于栈是一个表,因此任何实现表的方法都可以用来实现栈。主要有两种方式,链表实现和数组实现。
- 使用链表方式实现的栈又叫动态栈。动态栈有链表的部分特性,即元素与元素之间在物理存储上可以不连续,但是功能有些受限制,动态栈只能在栈顶处进行插入和删除操作,不能在栈尾或栈中间进行插入和删除操作。
- 使用数组实现栈叫做静态栈。
2.1 用数组实现栈
数组栈:
package stackPractice;
public class ArrayStack {
//栈的大小
private int maxStack;
//定义数组来模拟栈
private int[] stack;
//表示栈顶所在的位置,默认情况下如果没有数据,使用-1
private int top = -1;
//构造方法:给出栈的具体大小
public ArrayStack(int maxStack) {
this.maxStack = maxStack;
//初始化当前数组的容量
stack = new int[maxStack];
}
//判断是否已经满栈
public boolean isFull(){
return this.top == this.maxStack - 1;
}
//判断当前栈是否为空栈
public boolean isEmpty(){
return this.top == -1;
}
//压栈
public void push(int val){
//是否已经栈满
if (isFull()){
throw new RuntimeException("此栈已满!");
}
top++;
stack[top] = val;
}
//出栈,弹栈
public int pop(){
//如果栈中是空
if (isEmpty()){
throw new RuntimeException("空栈,未找到数据!");
}
int value = stack[top];
top--;
return value;
}
//查看栈中所有元素
public void list(){
//是否是空栈
if (isEmpty()){
throw new RuntimeException("空栈,未找到数据!");
}
for (int i = 0; i < stack.length; i++) {
System.out.printf("stack[%d] = %d\n",i,stack[i]);
}
}
//栈中元素存在的个数
public int length(){
return this.top + 1;
}
}
2.2 判断回文
回文:一个数据从左往右和从右往左都是一样的 例如:aba;racecar
需求:通过上面以数组模拟栈来判断一个字符串是否是一个回文数据
package stackPractice;
public class stackPracticeTest01 {
public static void main(String[] args) {
System.out.println(detecation("hello"));//输出为:false
System.out.println(detecation("aba"));//输出为:true
}
public static boolean detecation(String val){
//初始化栈对象
ArrayStack arrayStack = new ArrayStack(10);
//获取字符串长度
int length = val.length();
//把字符串数据逐次获取字符压栈至数组中
for (int i = 0; i < length; i++) {
arrayStack.push(val.charAt(i));
}
//获取
String newVal = "";
int length1 = arrayStack.length();//需要将该代码从for循环中提出来,因为随着top的移动length()值也会改变
for (int i = 0; i < length1; i++) {
//是否是一个空栈
if (!arrayStack.isEmpty()){
char pop = (char)arrayStack.pop();//弹出的是数字,强转为char类型
newVal = newVal + pop;
}
}
if (val.equals(newVal)){
return true;
}
return false;
}
}
2.3 面试题:使用栈完成一个表达式计算结果
需求:计算 String val = "4 + 3 + 2 + 1 * 5"的结果
- 循环遍历出来每一个元素
- 如果是数字则压入数字栈,如果是一个运算符则压入运算符栈
- 如果是运算符:
如果运算符栈中是空,则直接入栈
如果运算符栈中不为空,先对比栈中运算符的优先级:
①如果优先级小于等于栈中的运算符,则需要把原运算符弹出,数字栈中数字进行弹栈,进行运算,将得到的结果重新放入数字栈中,再把新运算符入运算符栈
②如果优先级大于原来的运算符栈中运算符,直接将运算符入栈即可,最终获取一个表达式结果
栈
package stackPractice;
public class ArrayStack {
//栈的大小
private int maxStack;
//定义数组来模拟栈
private int[] stack;
//表示栈顶所在的位置,默认情况下如果没有数据,使用-1
private int top = -1;
//构造方法:给出栈的具体大小
public ArrayStack(int maxStack) {
this.maxStack = maxStack;
//初始化当前数组的容量
stack = new int[maxStack];
}
//判断是否已经满栈
public boolean isFull(){
return this.top == this.maxStack - 1;
}
//判断当前栈是否为空栈
public boolean isEmpty(){
return this.top == -1;
}
//压栈
public void push(int val){
//是否已经栈满
if (isFull()){
throw new RuntimeException("此栈已满!");
}
top++;
stack[top] = val;
}
//出栈,弹栈
public int pop(){
//如果栈中是空
if (isEmpty()){
throw new RuntimeException("空栈,未找到数据!");
}
int value = stack[top];
top--;
return value;
}
//查看栈中所有元素
public void list(){
//是否是空栈
if (isEmpty()){
throw new RuntimeException("空栈,未找到数据!");
}
for (int i = 0; i < stack.length; i++) {
System.out.printf("stack[%d] = %d\n",i,stack[i]);
}
}
//栈中元素存在的个数
public int length(){
return this.top + 1;
}
//判断是否是一个运算符 + - * /
public boolean isOper(char v){
return v == '+' ||v == '-' ||v == '*' ||v == '/';
}
//判断运算符优先级 使用数字表示优先级大小,数字越大的优先级越大
public int priority(int oper){
if (oper == '*' ||oper == '/'){
return 1;
}else if (oper == '+' ||oper == '-'){
return 0;
}else {
return -1;
}
}
//获取栈顶数据
public int peek(){
return this.stack[top];
}
//获取栈的容量
public int stackLength(){
return this.stack.length;
}
//计算两个数进行运算后的结果
//比如: 2 + 3
//弹栈的时候,先3出来,后2出来,所以此时num1 = 3,num2 = 2
public int calculate(int num1,int num2,int oper){
//计算结果
int result = 0;
switch (oper){
case '+':
result = num1 + num2;
break;
case '-':
result = num2 - num1;
break;
case '*':
result = num1 * num2;
break;
case '/':
result = num2 / num1;
break;
default:
break;
}
return result;
}
}
测试代码
package stackPractice;
public class stackPracticeTest02 {
public static void main(String[] args) {
String str = "4+3+2*3-1";
//数字栈
ArrayStack numStack = new ArrayStack(10);
//运算符栈
ArrayStack symbolStack = new ArrayStack(10);
//定义变量参数
int temp1 = 0;
int temp2 = 0;
int symbolChar = 0;
int result = 0;
//获取字符串长度
int length = str.length();
//字符的拼接,比如:33
String values = "";
for (int i = 0; i < length; i++) {
//获取每一个字符
char c = str.charAt(i);
//是否是一个运算符
if (symbolStack.isOper(c)){
//如果不是一个空运算符栈
if (!symbolStack.isEmpty()){
//比较运算符的优先级
if (symbolStack.priority(c) <= symbolStack.priority(symbolStack.peek())){
//去运算符栈中获取栈顶的运算符
//去数字栈中获取两个数字
temp1 = numStack.pop();
temp2 = numStack.pop();
symbolChar = symbolStack.pop();
result = numStack.calculate(temp1, temp2, symbolChar);
//把运算结果再次放入数字栈中
numStack.push(result);
//把当前的运算符压入运算符栈中
symbolStack.push(c);
}else {
symbolStack.push(c);
}
}else {
//如果是空运算符栈,将运算符直接压栈
symbolStack.push(c);
}
}else {
//比如:33+44
values += c;
if (i == length - 1){//如果是最后一个,直接压栈
numStack.push(Integer.parseInt(values));
}else {
//如果不是最后一个,将下一个取出
char data = str.substring(i + 1,i + 2).charAt(0);
//上面等价于:char data = str.charAt(i + 1);
//判断当前的是不是一个运算符
if (symbolStack.isOper(data)){
//如果是运算符,就认为当前的数字结束了,进行压栈
numStack.push(Integer.parseInt(values));
values = "";
}
}
}
}
while (true){
if (symbolStack.isEmpty()){
break;
}
temp1 = numStack.pop();
temp2 = numStack.pop();
symbolChar = symbolStack.pop();
result = numStack.calculate(temp1, temp2, symbolChar);
numStack.push(result);
}
int res = numStack.pop();
System.out.println("结果是:" + res);
}
}
3.链表
链表是一种物理存储单元上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的
特点:
- 链表是以结点形式存储的,是链式存储
- 每个节点包含data区域和next区域
- 各个结点并不是连续存储的
3.1 单链表
需求:根据带有头部的链表,实现商品增删改查,并且也可以针对商品已编号进行排序,完成排行榜
商品节点:
package linkedPractice;
public class GoodsNode {
public int id;
public String name;
public double price;
public GoodsNode next;
public GoodsNode(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
链表类:
package linkedPractice;
public class DLLinkedList {
private GoodsNode node = new GoodsNode(0,"",0.0);
//往链表尾部中添加结点
public void add(GoodsNode goodsNode){
GoodsNode temp = node;
while (true){
if (temp.next == null){
break;
}
temp = temp.next;
}
temp.next = goodsNode;
}
//按照商品编号id值进行添加,从小到大的顺序添加
public void addOrder(GoodsNode goodsNode){
GoodsNode temp = node;
//标记id相等的情况
boolean flg = false;
while (true){
if (temp.next == null){
break;
}
if (temp.next.id > goodsNode.id){
break;
}else if(temp.next.id == goodsNode.id){
flg = true;
break;
}
temp = temp.next;
}
if (flg){
System.out.println("已经存在了该商品,不能添加重复元素!");
}else {
goodsNode.next = temp.next;
temp.next = goodsNode;
}
}
//修改结点
//1.先找到链表中目标结点 2.根据新的数据修改
public void updateNode(GoodsNode goodsNode){
//如果链表空
if (node.next == null){
System.out.println("链表为空...");
return;
}
GoodsNode temp = node.next;
//标识符,表示找到了结点
boolean flg = false;
while (true){
//直到链表中的最后一个结点未找到
if (temp == null){
break;
}
//找到结点,循环结束
if (temp.id == goodsNode.id){
flg = true;
break;
}
//拿下一个元素,继续往下找
temp = temp.next;
}
if (flg){
//真正的修改结点
temp.name = goodsNode.name;
temp.price = goodsNode.price;
}else {
System.out.println("在整个链表中未找到目标结点....");
}
}
//结点删除功能
//条件:根据结点的编号删除
public void delNode(int id){
GoodsNode temp = node;
//标识符,表示找到了结点
boolean flg = false;
while (true){
if (temp.next == null){
break;
}
if (temp.next.id == id){
flg = true;
break;
}
//拿下一个元素,继续往下找
temp = temp.next;
}
if (flg == true){
temp.next = temp.next.next;
}else {
System.out.println("未找到删除的结点...");
}
}
//查询功能
//查看链表中每一个结点元素
public void list(){
if (node.next == null){
System.out.println("空链表");
return;
}
GoodsNode temp = node.next;
while (true){
if (temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
测试类:
package linkedPractice;
public class LinkedTest {
public static void main(String[] args) {
DLLinkedList linkedList1 = new DLLinkedList();
DLLinkedList linkedList2 = new DLLinkedList();
GoodsNode goodsNode1 = new GoodsNode(1,"运动鞋",599.50);
GoodsNode goodsNode2 = new GoodsNode(2,"上衣",399.50);
GoodsNode goodsNode3 = new GoodsNode(3,"短裤",50.50);
GoodsNode goodsNode4 = new GoodsNode(4,"拖鞋",150.50);
linkedList1.add(goodsNode1);
linkedList1.add(goodsNode2);
linkedList1.add(goodsNode3);
linkedList1.add(goodsNode4);
linkedList1.list();
//这里会有问题:在上面链表链接后,在这里只要将第一个链上,后面2,3,4自动链上的
System.out.println("===================================");
linkedList2.addOrder(goodsNode3);
linkedList2.addOrder(goodsNode1);
linkedList2.addOrder(goodsNode4);
linkedList2.addOrder(goodsNode2);
linkedList2.list();
System.out.println("===================================");
linkedList2.updateNode(new GoodsNode(2,"太阳帽",200));
linkedList2.list();
System.out.println("===================================");
linkedList2.delNode(3);
linkedList2.list();
}
}
运行结果:
3.2 单链表面试题
需求:计算单链表中存在的结点个数,不统计头结点
在上面的链表类中添加方法:
//计算单链表中存在的结点个数【不统计头结点】
public int getLength(){
if (node.next == null){
System.out.println("空链表");
return 0;
}
GoodsNode temp = node.next;
int length = 0;
while (temp != null){
//结点个数
length++;
temp = temp.next;
}
return length;
}
测试代码:
System.out.println(linkedList1.getLength());
3.3 双链表
- 双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱
- 从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点
书本结点:
package linkedPractice;
public class BookNode {
public int id;
public String name;
public double price;
//结点下一个结点,直接后继
public BookNode next;
//上一个结点,直接前驱
public BookNode pre;
public BookNode(int id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
}
双向链表:
package linkedPractice;
public class DualLinkedList {
//创建空的头结点
private BookNode head = new BookNode(0,"",0.0);
//向尾部添加新的结点
public void addLast(BookNode newNode){
BookNode temp = head;
while (true){
//找到最后一个结点
if (temp.next == null){
break;
}
temp = temp.next;
}
//新的结点作为上一个尾部结点的直接后继
//尾部结点作为新的结点的直接前驱
temp.next = newNode;
newNode.pre = temp;
newNode.next = null;
}
//修改结点
//条件:双向链表中的每一个结点的id和修改的id对比,如果对比成功,则进行修改该结点
//如果没有对比成功,双向链表中未找到目标结点
public void updateNode(BookNode node){
//是否是空链表
if (head.next == null){
System.out.println("空链表...");
return;
}
BookNode temp = head.next;
boolean flg = false;
while (true){
if (temp == null){
break;
}
if (temp.id == node.id){
flg = true;
break;
}
temp = temp.next;
}
if (flg){
temp.name = node.name;
temp.price = node.price;
}else {
System.out.println("未找到需要修改的结点...");
}
}
//双向链表的删除
//条件:根据id编写进行删除结点
public void delNode(int id){
//是否是空链表
if (head.next == null){
System.out.println("空链表...");
return;
}
BookNode temp = head.next;
boolean flg = false;
while (true){
if (temp == null){
break;
}
if (temp.id == id){
flg = true;
break;
}
temp = temp.next;
}
if (flg){
temp.pre.next = temp.next;
if (temp.next != null){
temp.next.pre = temp.pre;
}
}else {
System.out.println("未找到需要删除的结点...");
}
}
}
测试类:
package linkedPractice;
public class LinkedTest02 {
public static void main(String[] args) {
DualLinkedList dualLinkedList = new DualLinkedList();
//创建结点
BookNode bookNode1 = new BookNode(1,"红楼梦",88.0);
BookNode bookNode2 = new BookNode(2,"西游记",88.0);
BookNode bookNode3 = new BookNode(3,"水浒传",88.0);
BookNode bookNode4 = new BookNode(4,"三国演义",88.0);
//添加操作
dualLinkedList.addLast(bookNode1);
dualLinkedList.addLast(bookNode2);
dualLinkedList.addLast(bookNode3);
dualLinkedList.addLast(bookNode4);
System.out.println("============================");
//删除操作
dualLinkedList.delNode(2);
System.out.println("============================");
//修改操作
dualLinkedList.updateNode(new BookNode(3,"小飞侠",520.00));
}
}
运行结果:
debug查看运行结果:
添加效果:
删除效果:
修改效果:
3.4 单向环形链表(约瑟夫问题)
约瑟夫问题【约瑟夫环】:
设编号为1,2,…,n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到 m 的那个人出列,它的下一位又从1开始报数,数到m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列
结点类:
package linkedPractice;
public class Boy {
//结点编号
private int no;
//指向下一个结点
private Boy next;
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
}
环形链表:
package linkedPractice;
public class CircleSingleLinkedList {
//首结点
private Boy first = new Boy(-1);
//构建环形链表
public void addBoy(int nums){
if(nums < 1){
System.out.println("数据不正确");
return;
}
//辅助结点
Boy temp = null;
for (int i = 1; i <= nums; i++) {
Boy boy = new Boy(i);
//判断是否是第一个孩子,如果是第一个需要自己与自己构成环形
if (i == 1){
first = boy;
first.setNext(first);
temp = first;
}else{
temp.setNext(boy);
boy.setNext(first);
temp = boy;
}
}
}
//查看环形链表中的结点
public void showBoy(){
if (first == null){
System.out.println("链表为空...");
return;
}
Boy boy = first;
while (true){
System.out.printf("小孩的编号%d\n",boy.getNo());
if (boy.getNext() == first){
break;
}
boy = boy.getNext();
}
}
/**
* 约瑟夫问题
* @param startNo 第几个孩子开始数数
* @param countNum 数几次
* @param nums 环形链表中一共有几个孩子
*/
public void countBoy(int startNo,int countNum,int nums){
if (first == null || startNo < 1 || startNo > nums){
System.out.println("参数输入有错误...");
return;
}
//定义辅助指针,指向的是环形单链表中的最后一个结点
Boy helper = first;
while (true){
if (helper.getNext() == first){
break;
}
helper = helper.getNext();
}
//寻找起始位置,把first定义为起始位置
for (int j = 0; j < startNo - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
//当孩子进行报数时,数到m的孩子进行出列
//让first和helper移动m - 1次即可,找到了出列的孩子
while (true){
if (helper == first){
break;
}
for (int j = 0; j < countNum - 1; j++) {
first = first.getNext();
helper = helper.getNext();
}
System.out.printf("孩子%d 出列\n",first.getNo());
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后出圈的孩子编号%d\n",first.getNo());
}
}
测试类:
package linkedPractice;
public class LinkedTest03 {
public static void main(String[] args) {
//定义环
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
//定义五个孩子
circleSingleLinkedList.addBoy(5);
//输出展示
circleSingleLinkedList.showBoy();
//约瑟夫问题模拟
circleSingleLinkedList.countBoy(1,2,5);
}
}
运行结果: