在IDE中编写源代码(.java文件),通过Java编译器(javac)将源代码编译为字节码文件并存储在外存/辅存中
运行Java程序时,JVM将字节码文件从外存/辅存加载至内存,JVM再通过解释器(Interpreter)或者即时编译器(JIT compiler)执行字节码
解释执行:逐条解释字节码并执行
JIT编译:将部分字节码编译为本地机器码(Native Machine Code针对特定硬件架构如x86,ARM编写的指令集,不需要进一步翻译或解释,是可直接被计算机硬件理解并执行的二进制指令),以提高执行效率
数据结构(data structure)是组织数据的方式 算法(algorithm):解决问题的方法
线性数据结构(linear data structure)
数据元素之间是一对一的线性关系,除第一个和最后一个元素外,都有一个前驱和一个后继元素
顺序存储结构(sequential storage structure)
数据元素按顺序存储在一块连续的空间,通过下标或索引直接访问,一旦定义了顺序存储结构的大小,例如定义了数组的长度,其大小就固定不变,无法动态扩展
可以通过下标快速访问数据;插入和删除元素时需要移动其他元素,性能较低;数组定义的大小大于实际需要的大小,可能会浪费空间,如果大小不足,则需要重新分配更大的空间并复制数据
例如:
数组(Array):用于存储一组类型相同的数据
动态数组(如C++中的vector,Java中的ArrayList):允许在运行时自动调整数组的大小,在数据量动态变化时非常有用
链式存储结构(Linked storage structure)
数据元素存储在不连续的空间中,每个元素称为结点(Node),结点包含数据域(Data)和一个或多个指向其他元素位置的指针(引用)(指针域Pointer,或曰next域),通过指针将多个结点连接在一起
对比顺序存储结构那样需要连续的空间,无需提前知道大小,可动态增删结点;在插入和删除操作只涉及到指针的改变,性能更好;缺点是增加了指针域,增加了存储开销;访问一个结点时,需要从头结点开始遍历,随机访问速度较慢
例如:
单向链表(singly linked list):每个结点只有一个指针指向下一个结点,最后一个结点的指针指向NULL
缺点是只能单向遍历,不能反向访问
双向链表(double linked list):每个结点有两个指针,一个指向下一个结点,一个指向上一个结点,支持双向遍历,相比单向链表,每个结点需要额外一个指针域,增加了存储开销
循环链表(circular linked list):链表最后一个结点指向头结点,可以是单向的或双向的
非线性数据结构(Non-linear data structure)
数据元素间没有线性关系,元素间可以是一对多的关系
树(Tree) 图(Map) 哈希表(Hash Table)
稀疏数组(Sparse Array)
是一种用于存储稀疏数据(Sparse Data大部分元素是零或者某个值,只有少部分元素是有意义的非零值)的数据结构
在存储稀疏数据的情景中,传统数组会浪费大量空间存储零值元素,而稀疏数组则通过只存储非零元素来压缩数组,节省空间
稀疏数组操作比普通数组复杂,可能需要维护更多的索引信息
如果需要频繁随机访问数组,由于需要解压缩元素的位置,稀疏数组可能比普通数组更慢
稀疏数组的基本结构
行号(Row Index):元素所在的行位置
列号(Column Index):元素所在的列位置
值(Value):元素的实际值(非零值)
稀疏数组的存储方式
三元组表示法(Triplet)
每个非零元素使用一个三元组(行号,列号,值)来表示,并将所有三元组存储在一个数组或其他结构中
压缩行存储(Compressed Row Storage,CRS)
将数据按行进行压缩,记录每一行中非零元素的位置和值
压缩列存储(Compressed Column Storage,CCS)
将数据按列进行压缩,记录每一列中非零元素的位置和值
应用场景
稀疏矩阵(Sparse Matrix)/二维数组,比如保存棋盘
public void SparseArrayDemo() {
//创建原始二维数组,除了[1][2]和[2][3],其他元素皆为0
int[][] arr = new int[11][11];
arr[1][2] = 1;
arr[2][3] = 2;
//遍历原始二维数组,获得非零数据个数
// 通过arr.length获得行数,通过arr[i].length获得每一行的列数
// int[][] arry = {
// {1, 2, 3},
// {1, 2},
// {1}
// };
int sum = 0;
for (int i = 0; i < arr.length; i++)
for (int j = 0; j < arr[i].length; j++)
if (arr[i][j] != 0) sum++;
//创建对应的稀疏数组,共sum+1行,sum行用于保存非零数据,第一行用于保存原始数组行数,列数及非零元素个数
//共三列,第一列用于保存非零元素在原始数组中的行号,第二列用于保存列号,第三列用于保存value
int[][] sparseArr = new int[sum + 1][3];
sparseArr[0][0] = arr.length;
sparseArr[0][1] = arr[0].length;
sparseArr[0][2] = sum;
int count = 1;
for (int i = 0; i < 11; i++)
for (int j = 0; j < 11; j++)
if (arr[i][j] != 0) {
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = arr[i][j];
count++;
}
//稀疏数组解压缩
int[][] arr2 = new int[sparseArr[0][0]][sparseArr[0][1]];
for (int i = 1; i < sparseArr.length; i++)
arr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
队列(Queue)
一种遵循先进先出(FIFO,first in first out)的线性数据结构,最早插入队列的元素最先被删除,插入操作发生在队尾(rear),删除操作发生在队头(front)(入队enqueue 出队dequeue)
用数组实现普通队列
用数组实现循环队列
class CircleArray {
//表示数组最大容量,即循环队列最大容量
private int maxSize;
//初始化为0;指向队列第一个元素
private int front = 0;
//初始化为0;指向队列最后一个元素的后一个位置
private int rear = 0;
private int[] arr;
public CircleArray(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize + 1];
}
//判断为空与否
public boolean isEmpty() {
return rear == front;
}
//判断为满与否
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
//求队列有效数据个数
public int size() {
return (rear - front + maxSize) % maxSize;
}
//添加数据
public void addQueue(int addedData) {
//判断数组为满与否
if (isFull()) {
System.out.println("数组已满");
return;
}
//添加数据
arr[rear] = addedData;
//rear指针移位
rear = (rear + 1) % maxSize;
}
//出队列
public int getQueue() {
//判断为空与否
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
//将将要出队列的第一个元素保存至temp中
int temp = arr[front];
//指针移位
front = (front + 1) % maxSize;
return temp;
}
}
链表(Linked List)
有头结点的链表:头结点不存储数据,只用于链表的管理
无头结点的链表:链表的第一个结点即为存储数据的结点
单向链表(Single Linked List)
class Node {
//数据域
public int id;
public String name;
//指针域
public Node next;
public Node(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
class singleLinkedList {
//初始化一个头结点,不存放数据,头结点永远不动
private Node head = new Node(0, "");
//获得头结点,即获得链表
public Node getHead(){
return head;
}
//根据id删除对应结点,找到需要删除的结点的前一个结点
public void deleteById(int id) {
//判断链表为空与否
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
Node tempPtr = head.next;
//flag用以标识链表中是否存在对应id
boolean flag = false;
while (true) {
if (tempPtr.next == null) break;
if (tempPtr.next.id == id) {
flag = true;
break;
}
tempPtr = tempPtr.next;
}
if (flag) {
tempPtr.next = tempPtr.next.next;
} else {
System.out.println("不存在对应结点");
}
}
//根据targetNode.id修改结点信息,将对应id的结点的name修改为targetNode.name
public void updateById(Node targetNode) {
//判断链表为空与否
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
Node tempPtr = head.next;
//flag用以标识链表中是否存在对应id
boolean flag = false;
while (true) {
if (tempPtr == null) break;
if (tempPtr.id == targetNode.id) {
flag = true;
break;
}
tempPtr = tempPtr.next;
}
if (flag) {
tempPtr.name = targetNode.name;
} else {
System.out.println("没有找到对应结点");
}
}
//向链表里添加结点,不考虑排序,直接添加至链表尾部
//这个method没考虑添加时id不重复
public void add(Node addedNode) {
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
Node tempPtr = head;
//遍历寻找链表最后一个元素
while (true) {
if (tempPtr.next == null) {
break;
}
//tempPtr指针移位
tempPtr = tempPtr.next;
}
//跳出循环后,tempPtr即指向链表的最后一个元素
tempPtr.next = addedNode;
}
//向链表里添加结点,以id进行排序
public void addById(Node addedNode) {
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
Node tempPtr = head;
//flag用以标识addedNode.id在链表中是否存在,true->存在
boolean flag = false;
//遍历寻找addedNode的插入位置,向tempPtr指针指向的结点后面添加
while (true) {
if (tempPtr.next == null) break;
if (tempPtr.next.id > addedNode.id) {
break;
} else if (tempPtr.next.id == addedNode.id) {
flag = true;
break;
}
tempPtr = tempPtr.next;
}
if (flag) {
System.out.println("id已经存在");
} else {
addedNode.next = tempPtr.next;
tempPtr.next = addedNode;
}
}
//显示链表
public void showLinkedList() {
//判断链表为空与否
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
Node tempPtr = head.next;
//遍历并输出链表
while (true) {
//链表为空时,直接break
if (tempPtr == null) break;
System.out.println(tempPtr);
//指针移位
tempPtr = tempPtr.next;
}
}
}
例题1
求出单链表中有效数据个数
解:遍历+计数器
例题2
寻找单链表中倒数第k个结点
解:先求出单链表中有效数据个数size,则正数size-k个结点即为所求
例题3
反转单链表
解:
//反转链表
public void reverseLinkedList(Node head) {
//原链表为空,或者只有一个结点,则无需操作原链表
//先判断head.next,否则先判断head.next.next可能直接报空指针
if (head.next == null || head.next.next == null) {
return;
}
//currentPtr用以遍历原链表
Node currentNode = head.next;
//nextNode指向currentNode在原始链表中的下一个结点
Node nextNode;
//tempHead临时头结点用以存储反转链表
//将遍历到的原链表上的每一个结点依次拼接到tempHead临时链表上
Node reverseHead = new Node(0, "");
while (currentNode != null) {
nextNode = currentNode.next;
currentNode.next = reverseHead.next;
reverseHead.next = currentNode;
currentNode = nextNode;
}
head.next = reverseHead.next;
}
例题4
逆序打印单链表
思路一:先反转,再打印,但破环了原始链表
思路二:将各个节点压入栈,利用栈先进后出的特性,即可逆序打印
public void reversePrint(Node head) {
if (head.next == null) {
System.out.println("链表为空");
}
//创建栈
Stack<Node> stack = new Stack<>();
//currentNode用于遍历原始链表
Node currentNode = head.next;
//将链表所有结点压入栈
while (currentNode != null) {
stack.push(currentNode);
currentNode = currentNode.next;
}
//依次出栈并打印即可
while (stack.size() > 0)
System.out.println(stack.pop());
}
例题5
合并两个有序单链表,合并后依旧有序
//参考题83的思路,合并两个有序数组,采用双指针法
//我们同样申请一个新链表用来存储最终结果
//代码为:ListNode resultNode = new resultNode();
//list1 和 list2作为指针分别指向两个原始链表,比较两个指针指向的结点之val以确定将哪个结点存入结果链表
//我们还需要一个指针用来告诉我们应该往新链表的哪一个位置上存(后面为了方便描述,就把这个指针叫做指示指针,命名为showNode)
//初始时,当然给resultNode(指向结果链表的头结点,且始终指向)里存
//所以指示指针一开始应该指向结果链表的头结点
//代码实现为:ListNode showNode = resultNode
//假设第一次填充的是list1指针指向的结点
//则代码实现为:
//首先把list1指针指向的结点存入结果链表:showNode.next = list1;
//然后移动list1指针,list1 = list1.next
//最后移动指示指针至下一次应当存入的位置
//showNode = showNode.next;
//以此类推,遍历尽两个原始链表
//若某个链表为空了,则直接把另一个链表拼入结果链表即可
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//防御性编程,如果哪一个链表为空,直接返回另一个链表即可
//另外:为什么要叫防御性编程?因为一旦为空,后面调用方法之类的就很可能报空指针异常
if (list1 == null) return list2;
if (list2 == null) return list1;
//创建结果链表
ListNode resultNode = new ListNode();
//创建并初始化指示指针
ListNode showNode = resultNode;
while (list1 != null && list2 != null) {//一旦哪一个链表为空,就跳出循环
if (list1.val < list2.val) {//判断大小,以决定把哪个存入结果链表
showNode.next = list1;
list1 = list1.next;
} else {
showNode.next = list2;
list2 = list2.next;
}
//移动指示指针:
showNode = showNode.next;
}
//判断哪一个链表空了.然后把另一个链表拼到结果链表里
if (list1 == null) showNode.next = list2;
if (list2 == null) showNode.next = list1;
//返回结果链表,这里实际上的结果链表的头结点应该是被resultNode.next指向的,所以返回resultNode.next
return resultNode.next;
}
//法二:递归写法
//考虑一个黑箱方法mergeTwoLists,其作用为传入两个原始链表(两个原始链表为了方便叙述,记作list1链表和list2链表),返回拼接后的链表
//递归需要缩小问题的规模,我们采用如下的缩小方法:
//比较list1链表和list2链表头结点的val,哪个头结点va小,则说明那个头结点的val是最小的
//我们可以把那个头结点去除,然后把list1链表和list2链表(其中一个去头了)再次传入黑箱方法
//代码实现为:mergeTwoLists(list1.next,list2)
//再把返回的链表和那个被去除的头结点进行拼接整体作为返回值
//代码实现为:list1.next = mergeTwoLists(list1.next,list2)
//返回值为:return list1;
//边界条件为:传入的某个原始链表为空,则直接返回另一个链表即可
//总的代码如下:
// public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// //边界条件
// if(list1 == null) return list2;
// if (list2 == null) return list1;
// if (list1.val < list2.val){
// list1.next = mergeTwoLists(list1.next,list2);
// return list1;
// }else {
// list2.next = mergeTwoLists(list2.next,list1);
// return list2;
// }
// }
双向链表(Double Linked List)
class DNode {
//数据域
public int id;
public String name;
//指针域
public DNode next;
public DNode pre;
public DNode(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "DNode{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
class DoubleLinkedList {
//头结点
private DNode head = new DNode(0, "");
//获取头结点
public DNode getHead() {
return head;
}
//遍历双向链表
public void list() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
DNode ptrNode = head.next;
while (true) {
if (ptrNode == null) break;
System.out.println(ptrNode);
ptrNode = ptrNode.next;
}
}
//添加结点,默认添加至尾部
public void add(DNode addedDNode) {
DNode ptrNode = head;
//遍历链表,找到最后一个结点
while (true) {
if (ptrNode.next == null) break;
//指针移位
ptrNode = ptrNode.next;
}
//添加结点
ptrNode.next = addedDNode;
addedDNode.pre = ptrNode;
//addedDNode.next = null;
}
//向链表里添加结点,以id进行排序
public void addById(DNode addedNode) {
//因为head不动,所以使用一个辅助指针tempPtr用于遍历链表
DNode tempPtr = head;
//flag用以标识addedNode.id在链表中是否存在,true->存在
boolean flag = false;
//遍历寻找addedNode的插入位置,向tempPtr指针指向的结点后面添加
while (true) {
tempPtr = tempPtr.next;
if (tempPtr == null) break;
if (tempPtr.id > addedNode.id) {
break;
} else if (tempPtr.id == addedNode.id) {
flag = true;
break;
}
}
if (flag) {
System.out.println("id已经存在");
} else {
tempPtr.pre.next = addedNode;
addedNode.next = tempPtr;
addedNode.pre = tempPtr.pre;
tempPtr.pre = addedNode;
}
}
//修改某一个链表的内容(即name属性)
public void upDateById(DNode targetDNode) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
DNode ptrNode = head.next;
//标识是否找到相应结点
boolean flag = false;
while (true) {
if (ptrNode == null) break;
if (ptrNode.id == targetDNode.id) {
flag = true;
break;
}
ptrNode = ptrNode.next;
}
if (flag) {
ptrNode.name = targetDNode.name;
} else {
System.out.println("未找到对应结点");
}
}
//根据Id删除结点
public void deleteById(int id) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
DNode ptrNode = head.next;
//用于标识是否存在对应Id的结点
boolean flag = false;
while (true) {
if (ptrNode == null) break;
//上下两个if,先判断ptrNode为空与否,否则先判断ptrNode.id==id会报空指针异常
if (id == ptrNode.id) {
flag = true;
break;
}
//指针移位
ptrNode = ptrNode.next;
}
if (flag) {
ptrNode.pre.next = ptrNode.next;
//ptrNode可能指向链表的最后一个结点,所以先加以判断,否则直接ptrNode.next.pre会报空指针异常
if (ptrNode.next != null)
ptrNode.next.pre = ptrNode.pre;
} else {
System.out.println("未找到对应结点");
}
}
}
环形链表
约瑟夫问题
n个人围成一圈,编号为1~n, 编号为k的人从1开始报数,数到m的人出列,出列的人的下一个人再从1开始报数, 以此类推, 求出列的人的编号序列
class Josephus {
//first结点用于管理环形链表,但亦存储数据
private Node first = null;
//初始化环形链表的大小,注意每次初始化得到的都是一个新的约瑟夫环
public void initializeCircleList(int nodeNums) {
//对nodeNums进行校验
if (nodeNums < 1) {
System.out.println("输入大于0的值");
return;
}
//辅助指针,指向上一次添加的结点
//这里可以不初始化的,只是IDEA比较智能,会检测出潜在的空指针异常的风险,实际上这个风险不会发生
Node currentNode = first;
for (int i = 1; i <= nodeNums; i++) {
//构建环形链表
Node addednode = new Node(i, "");
//第一个结点比较特殊
//让first指向第一个结点,之后,first始终指向第一个结点
if (i == 1) {
first = addednode;
first.next = first;
currentNode = first;
} else {
currentNode.next = addednode;
addednode.next = first;
currentNode = addednode;
}
}
}
//显示上次初始化的约瑟夫环形链表
public void showList() {
if (first == null) {
System.out.println("链表为空");
return;
}
Node ptrNode = first;
while (true) {
System.out.println(ptrNode.id);
ptrNode = ptrNode.next;
if (ptrNode == first) break;
}
}
//出列序列
//startId:开始数数的人 countNum:一轮数几下 nodeNums:一开始总共有多少人
public void outSequence(int startId, int countNum, int nodeNums) {
//校验参数
if (first == null || startId < 1 || startId > nodeNums) {
System.out.println("参数输入有误");
return;
}
//创建辅助指针helper,ptrNode
Node helper = first;
Node ptrNode = first;
//先让helper指针移至first结点的前一个结点
while (true) {
if (helper.next == first) break;
helper = helper.next;
}
//再让两个辅助指针移动到起始位置上
for (int i = 1; i <= startId - 1; i++) {
helper = helper.next;
ptrNode = ptrNode.next;
}
//开始报数
while (true) {
//说明圈中只剩一个结点了
if (helper == ptrNode) break;
for (int i = 1; i <= countNum - 1; i++) {
//让两个指针同时移动countNum-1次,此时ptrNode即指向应出列之结点
helper = helper.next;
ptrNode = ptrNode.next;
}
//输出出列的人的id
System.out.println(ptrNode.id);
ptrNode = ptrNode.next;
helper.next = ptrNode;
}
//输出最后出列的人的id
System.out.println(ptrNode.id);
}
}
栈(Stack)
用数组模拟栈
class Stack {
//栈的大小
private int maxSize;
//用以模拟栈的数组
private int[] arr;
//表示栈顶,初始化为-1,表示不存在数据
private int top = -1;
public Stack(int maxSize) {
this.maxSize = maxSize;
arr = new int[this.maxSize];
}
//返回栈顶元素,但不是pop
public int peek() {
return arr[top];
}
//判断栈满
public boolean isFull() {
return top == maxSize - 1;
}
//判断栈空
public boolean isEmpty() {
return top == -1;
}
//入栈
public void push(int pushedValue) {
if (isFull()) {
System.out.println("栈满");
return;
}
top++;
arr[top] = pushedValue;
}
//出栈
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
int poppedValue = arr[top];
top--;
return poppedValue;
}
//遍历显示栈
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = top; i >= 0; i--)
System.out.println(arr[i]);
}
//返回运算符的优先级,优先级使用数字表示,数字越大,优先级越高
public int priority(int operator) {
if (operator == '*' || operator == '/') {
return 1;
} else if (operator == '+' || operator == '-') {
return 0;
} else {
return -1;
}
}
//判断是不是一个运算符
public boolean isOperator(char operator) {
return operator == '+' || operator == '-'
|| operator == '*' || operator == '/';
}
//计算方法
public int calculate(int operand1, int operend2, int operator) {
//初始化为0,这个不严谨,但是不初始化会报错,因为后面return result了
int result = 0;
switch (operator) {
case '+':
result = operend2 + operand1;
break;
case '-':
result = operend2 - operand1;
break;
case '*':
result = operend2 * operand1;
break;
case '/':
result = operend2 / operand1;
break;
default:
break;
}
return result;
}
}
三种表达式
前缀表达式(prefix expression):运算符位于操作数之前
中缀表达式(infix expression):常见式子
后缀表达式(suffix expression):又称逆波兰表达式,运算符位于操作数之后
例题1:用栈计算中缀表达式
这里用的栈是上面自定义的栈,因为需要自己定义一些方法,比如priority
//这个计算器只有+-*/,没有()
//创建一个数栈,一个符号栈
//对targetExpression进行扫描
//如果是数字,则入数栈
//如果是符号:
//若符号栈为空,则直接入栈;
//若符号栈非空,则若当前扫描到的符号之优先级小于等于栈顶符号之优先级,
// 则从数栈中pop出两个数,再从符号栈中pop出一个符号,进行运算,将结果入数栈
// 最后将当前扫描到的符号入符号栈
// 若当前扫描到的符号之优先级大于栈顶符号之优先级
// 则直接入符号栈
//表达式扫描完毕后,从数栈pop出两个数,符号栈中pop出一个符号,并运算
//注意减法/除法中,后出栈的数是被减数/被除数
//最后数栈只剩下一个数字,即为最终结果
public int calculator(String targetExpression) {
//创建数栈和符号栈
Stack operandStack = new Stack(10);
Stack operatorStack = new Stack(10);
//用于扫描targetExpression的指针
int ptr = 0;
//用于存储扫描到的符号
char character;
//用于接收operand和operator和中间运算结果
int operand1;
int operand2;
int operator;
int result;
//用于处理多位数,不初始化会报错
String multiDigit = "";
while (true) {
character = targetExpression.substring(ptr, ptr + 1).charAt(0);
//判断character是数还是运算符
if (operatorStack.isOperator(character)) {
//判断符号栈为空与否
if (!operatorStack.isEmpty()) {
//判断当前扫描到的运算符和符号栈栈顶运算符的优先级
if (operatorStack.priority(character) <= operatorStack.priority(operatorStack.peek())) {
operand1 = operandStack.pop();
operand2 = operandStack.pop();
operator = operatorStack.pop();
result = operandStack.calculate(operand1, operand2, operator);
//result入数栈
operandStack.push(result);
//当前扫描到的运算符入符号栈
operatorStack.push(character);
} else {
operatorStack.push(character);
}
} else {
//符号栈为空,直接入栈
operatorStack.push(character);
}
} else {//如果是数,直接入数栈,但要处理多位数的情况
multiDigit += character;
if (ptr == targetExpression.length() - 1) {//如果character已经是targetExpression的最后一个字符
//则直接入栈
operandStack.push(Integer.parseInt(multiDigit));
} else {
//判断下一个字符是否为数字
if (operatorStack.isOperator(targetExpression.substring(ptr + 1, ptr + 2).charAt(0))) {
operandStack.push(Integer.parseInt(multiDigit));
//要重置multiDigit!!
multiDigit = "";
}
}
}
//ptr移位,并判断是否扫描完毕
ptr++;
if (ptr >= targetExpression.length()) break;
}
//表达式扫描完毕后,从数栈pop出两个数,符号栈中pop出一个符号,并运算
while (true) {
//如果符号栈为空,则计算完毕,数栈中仅有一个数字,即为最终结果
if (operatorStack.isEmpty()) {
break;
} else {
operand1 = operandStack.pop();
operand2 = operandStack.pop();
operator = operatorStack.pop();
result = operandStack.calculate(operand1, operand2, operator);
operandStack.push(result);
}
}
return operandStack.peek();
}
中缀表达式转后缀表达式
虽然会出不存在该运算符,但无伤大雅
//1)初始化两个栈,s1和s2
//2)从左向右扫描中缀表达式
//3)遇到操作数,则入栈s2
//4)遇到运算符
//4.1若s1为空,或s1栈顶元素为左括号(,或优先级比s1栈顶运算符高则直接入栈
//4.2若不满足4.1,则将s1栈顶元素弹出并压入s2,再次回到4.1
//5)
//5.1遇到左括号(,则直接入栈s1
//5.2遇到右括号),则依次弹出s1的元素并压入s2,直至遇到左括号,这对括号被丢弃
//怎么丢弃见代码
//6)重复步骤2~5,直至中缀表达式扫描完毕
//7)将s1中剩余的元素依次弹出并压入s2
//8)依次弹出s2中元素并输出,结果的逆序即为所求的后缀表达式
public List<String> infixToSuffix(String infixExpression) {
//先将infixExpression存储至list集合中,这里的infixExpression并无空格间隔
List<String> list = new ArrayList<>();
//用于遍历infixExpression
int ptr = 0;
//存储遍历到的字符
char character;
//用于处理多位数
String multiDigit;
do {
//如果character是一个非数字则直接加入list集合
if ((character = infixExpression.charAt(ptr)) < 48 ||
(character = infixExpression.charAt(ptr)) > 57) {
list.add("" + character);
ptr++;
} else {//如果是一个数,则需要考虑多位数
multiDigit = "";
while (ptr < infixExpression.length() && (character = infixExpression.charAt(ptr)) >= 48
&& (character = infixExpression.charAt(ptr)) <= 57) {
multiDigit += character;
ptr++;
}
list.add(multiDigit);
}
} while (ptr <= infixExpression.length() - 1);
//infixExpression存入list集合完毕
//定义两个栈s1,s2,因为s2栈没有出栈操作,所以s2使用集合List代替
//s2使用集合,就没有栈先入先出的限制,直接正常打印集合即是对应的后缀表达式
Stack<String> s1 = new Stack<>();
List<String> s2 = new ArrayList<>();
//遍历list
for (String item : list) {
if (item.matches("\\d+")) {//多位数
s2.add(item);
} else if (item.equals("(")) {//如果是左括号
s1.push(item);
} else if (item.equals(")")) {//如果是右括号,注意右括号没加入s2
while (!s1.peek().equals("(")) {//未见到左括号则s1不断pop
s2.add(s1.pop());
}
s1.pop();//将左括号弹出栈,左括号亦未加入s2
} else {
//4)遇到运算符
//4.1若s1为空,或s1栈顶元素为左括号(,或优先级比s1栈顶运算符高则直接入栈
//4.2若不满足4.1,则将s1栈顶元素弹出并压入s2,再次回到4.1
while (s1.size() != 0 && Operator.getPriority(s1.peek()) >= Operator.getPriority(item)) {
s2.add(s1.pop());
}
//出while循环说明满足4.1了,将item入栈s1
s1.push(item);
}
}
//7)将s1中剩余的元素依次弹出并压入s2
while (s1.size() != 0) {
s2.add(s1.pop());
}
return s2;
}
逆波兰计算器
//后缀表达式计算器,逆波兰计算器
//从左向右扫描字符串
//遇到数字则将数字入栈
//遇到运算符则pop两个数字进行相应的运算,并将结果入栈
//直至字符串扫描完毕,栈中唯一数字即为结果
//注意这里的suffixExpression中的数字符号之间用空格分开
public int polandCalculator(String suffixExpression) {
//将suffixExpression以space分割开,用list集合接收结果
String[] split = suffixExpression.split(" ");
List<String> list = new ArrayList<>();
for (String element : split) list.add(element);
Stack<String> stack = new Stack<>();
for (String item : list) {
//使用正则表达式取出数
if (item.matches("\\d+")) {//匹配多位数
stack.push(item);
} else {
//pop出两个数并运算,再将结果入栈
int operand2 = Integer.parseInt(stack.pop());
int operand1 = Integer.parseInt(stack.pop());
//存储运算结果
int result = 0;
if (item.equals("+")) {
result = operand1 + operand2;
} else if (item.equals("-")) {
result = operand1 - operand2;
} else if (item.equals("*")) {
result = operand1 * operand2;
} else if (item.equals("/")) {
result = operand1 / operand2;
} else {
throw new RuntimeException("运算符有误");
}
//将result入栈
stack.push("" + result);
}
}
return Integer.parseInt(stack.pop());
}
排序(Sort)
内部排序:数据量较小时,将数据全部加载至内存完成排序,速度较快
外部排序:数据量较大时,借助外存完成排序,涉及内外存的频繁交互,速度较慢
时间频度T(n),表示算法执行的基本操作总数,n是输入规模
渐进时间复杂度,简称时间复杂度,对操作执行次数忽略系数,忽略低阶...
最坏时间复杂度/平均时间复杂度
空间复杂度,算法运行时占用的空间大小的度量
相比空间复杂度,速度更重要,一些缓存产品(redis)和算法采取以空间换时间的策略
冒泡排序(Bubble Sort)
//O(n²)
public void bubbleSort(int[] arr) {
//第一轮确定前n个数里最大的数,头指针从索引0移动到索引n-2
//第二轮确定前n-1个数里最大的数,指针从索引0移动索引n-3
//第n-1轮确定前2个数里最大的数,指针从索引0移动索引0
//所以共进行n-1轮冒泡
//第i轮确定前n-i+1个数里最大的数,指针从索引0移动索引n-i-1次
int temp;
//用来标识如果在某轮冒泡排序中,没有进行过交换,则说明已经排序完成
boolean flag = false;
for (int i = 1; i <= arr.length - 1; i++) {
for (int j = 0; j <= arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
if (!flag) {
break;
} else {
//重置flag
flag = false;
}
}
}
选择排序
//O(n²)
public void selectSort(int[] arr) {
//共进行n-1轮选择排序
//第一轮确定后n个数(索引从0至n-1)里最小的数,和第1个数(索引为0)交换,假定最小的数是第1个(索引为0),指针从第1个数(索引为0)移动到最后一个数(索引为n-1)
//第二轮确定后n-1个数(索引从1至n-1)里最小的数,和第2个数(索引为1)交换,假定最小的数是第2个(索引为1),指针从第2个数(索引为1)移动到最后一个数(索引为n-1)
//第i轮确定后n-i+1个数(索引从i-1至n-1)里最小的数,和第i个数(索引为i-1)交换,假定最小的数是第i个(索引为i-1),指针从第i个数(索引为i-1)移动到最后一个数(索引为n-1)
int temp;
for (int i = 1; i <= arr.length - 1; i++) {
int minIndex = i - 1;
for (int j = i - 1; j <= arr.length - 1; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
temp = arr[minIndex];
arr[minIndex] = arr[i-1];
arr[i-1] = temp;
}
}
插入排序
//O(n²) but 优势在于1+2+3+...而非倒序相加
public void insertSort(int[] arr) {
//共进行n-1轮插入排序
//第一轮将前一个数看作有序表,其后看作无序表,将无序表第一个数(索引为1)插入有序表中
//第二轮将前二个数看作有序表,其后看作无序表,将无序表第一个数(索引为2)插入有序表中
//第i轮将前i个数看作有序表,其后看作无序表,将无序表第一个数(索引为i)插入有序表中
//用temp保存无序表第一个数
int temp;
//用于遍历有序表的指针
int ptr;
for (int i = 1; i <= arr.length - 1; i++) {
temp = arr[i];
ptr = i - 1;
//先判断temp < arr[ptr]得话可能arr[ptr]就报数组越界了
while (ptr >= 0 && temp < arr[ptr]) {
arr[ptr + 1] = arr[ptr];
ptr--;
}
//ptr+1即是应该插入的位置
arr[ptr + 1] = temp;
}
}
希尔排序
交换式
public void shellSort(int[] arr) {
//temp作为完成交换操作的中间变量
int temp;
//arr.length不断/2作为步长(或曰增量)gap,一共要进行log轮,直至gap为0
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i <= arr.length - 1; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
采用插入排序式
public void shellSort(int[] arr) {
//arr.length不断/2作为步长(或曰增量)gap,一共要进行log轮,直至gap为0
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i <= arr.length - 1; i++) {
//temp用于保存待插入元素
int temp = arr[i];
//ptr用以遍历分组
int ptr = i - gap;
while (ptr >= 0 && temp < arr[ptr]) {
arr[ptr + gap] = arr[ptr];
ptr -= gap;
}
//ptr+gap即为应该插入的位置
arr[ptr + gap] = temp;
}
}
}
快速排序(Quick Sort)
public void quickSort(int[] arr, int left, int right) {
int l = left;
int r = right;
int pivot = arr[(left + right) / 2];
int temp;
while (l < r) {
while (arr[l] < pivot) l += 1;
while (arr[r] > pivot) r -= 1;
if (l >= r) break;
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//防止出现如 pivot和其左右一个元素三者相等(pivot和其左或有一个原二者相等也会如此),结果l和r指针停着不动,出现死循环
if (arr[l] == pivot) r-=1;
if (arr[r] == pivot) l+=1;
}
if (l==r){
l+=1;
r-=1;
}
//向左递归
if (left<r){
quickSort(arr,left,r);
}
//向右递归
if (right>l){
quickSort(arr,l,right);
}
}
归并排序(Merge Sort)
//temp临时数组和arr长度相同
public void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;//
mergeSort(arr, left, mid, temp);//向左递归
mergeSort(arr, mid + 1, right, temp);//向右递归
merge(arr, left, right, mid, temp);//分解后即合并
}
}
//以mid为分界线,将arr划分为左右两个有序序列
public void merge(int[] arr, int left, int right, int mid, int[] temp) {
//指向temp数组的指针
int tempPtr = 0;
//i作为左边有序序列的指针
int i = left;
//j作为右边有序序列的指针
int j = mid + 1;
//while循环用于将左右两侧有序数组按序填充至temp数组
//直至左右某个序列填充完毕
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[tempPtr] = arr[i];
i++;
tempPtr++;
} else {
temp[tempPtr] = arr[j];
j++;
tempPtr++;
}
}
//将尚未被填充完毕的序列填充至temp数组
while (i <= mid) {//i<=mid说明左序列仍未被填充完毕
temp[tempPtr] = arr[i];
i++;
tempPtr++;
}
while (j <= right) {//j<=right说明右序列仍未被填充完毕
temp[tempPtr] = arr[j];
j++;
tempPtr++;
}
//将temp数组拷贝至arr
tempPtr = 0;//重置tempPtr指针
int arrPtr = left;//arrPtr指向arr
while (arrPtr <= right) {
arr[arrPtr] = temp[tempPtr];
tempPtr++;
arrPtr++;
}
}
查找
线性查找(Linear Search)
//O(n)
public List<Integer> linearSearch(int[] arr, int target) {
List<Integer> reasultList = new ArrayList<>();
for (int i = 0; i <= arr.length - 1; i++)
if (arr[i] == target) reasultList.add(i);
return reasultList;
}
二分查找(Binary Search)
//O(logn)
public List<Integer> binarySearch(int[] arr, int target, int left, int right) {
//递归边界条件,触发该条件,则说明target不存在
if (left > right || target < arr[0] || target > arr[arr.length - 1]) return new ArrayList<>();
int middle = (left + right) / 2;
if (arr[middle] < target) {
return binarySearch(arr, target, middle + 1, right);
} else if (arr[middle] > target) {
return binarySearch(arr, target, left, middle - 1);
} else {
//结果集合
List<Integer> reasult = new ArrayList<>();
//用于遍历数组,将所有等于target的元素都找出来
int temp;
//先向左遍历
temp = middle - 1;
while (true) {
if (temp < 0 || arr[temp] != target) break;
reasult.add(temp);
temp--;
}
reasult.add(middle);
//向右遍历
temp = middle + 1;
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) break;
reasult.add(temp);
temp++;
}
return reasult;
}
}
插值查找(Interpolation Search)
Interpolate: insert something of different nature into something else
插值查找是对二分查找的改进,适用于数据分布均匀时


//O(logn)
public List<Integer> interpolationSearch(int[] arr, int target, int left, int right) {
//递归边界条件,触发该条件,则说明target不存在
//target < arr[0] || target > arr[arr.length - 1]必须要有,否则可能数组越界
if (left > right || target < arr[0] || target > arr[arr.length - 1]) return new ArrayList<>();
int middle = left + (right - left) * (target - arr[left]) / (arr[right] - arr[left]);
if (arr[middle] < target) {
return interpolationSearch(arr, target, middle + 1, right);
} else if (arr[middle] > target) {
return interpolationSearch(arr, target, left, middle - 1);
} else {
//结果集合
List<Integer> reasult = new ArrayList<>();
//用于遍历数组,将所有等于target的元素都找出来
int temp;
//先向左遍历
temp = middle - 1;
while (true) {
if (temp < 0 || arr[temp] != target) break;
reasult.add(temp);
temp--;
}
reasult.add(middle);
//向右遍历
temp = middle + 1;
while (true) {
if (temp > arr.length - 1 || arr[temp] != target) break;
reasult.add(temp);
temp++;
}
return reasult;
}
}
斐波那契查找(Fibonacci Search)
public int fibonacciSearch(int[] arr, int target) {
//获得Fibonacci数列,这里聊取前20项Fibnacci
int[] fibSequence = new int[50];
fibSequence[0] = 0;
fibSequence[1] = 1;
for (int i = 2; i < 50; i++) {
fibSequence[i] = fibSequence[i - 1] + fibSequence[i - 2];
}
int low = 0;
int high = arr.length - 1;
int k = 0;
int middle;
while (fibSequence[k] <= arr.length) k++;
int[] tempArray = Arrays.copyOf(arr, fibSequence[k]);
for (int i = arr.length; i < tempArray.length; i++) {
tempArray[i] = arr[arr.length - 1];
}
while (low <= high) {
middle = low + fibSequence[k - 1] - 1;
if (target < tempArray[middle]) {
high = middle - 1;
k -= 1;
} else if (target > tempArray[middle]) {
low = middle + 1;
k -= 2;
} else {
if (middle <= high) {
return middle;
} else {
return high;
}
}
}
return -1;
}
哈希表(Hash Table,或曰散列表)
基于键值对(Key-Value Pair)的数据结构,Key是唯一的,通过哈希函数(Hash Function,或曰散列函数)计算出哈希值(或曰散列值),对应数组索引(哈希函数设计不当,会导致冲突增加,影响性能)
增删查很高效,平均TC为O(1),最坏情况下(如冲突严重时)可能退化为O(n)
哈希冲突(Collision):
拉链法(Chaining):在冲突位置存储一个链表或其他数据结构
链表越短越高效
开放地址法(Open Addressing):发生冲突时在其他空闲位置存储数据
线性探查(Linear Probing)
二次探查(Quadratic Probing)
双重哈希(Double Hashing)
动态扩展:
哈希表负载因子(元素数量和数组容量之比)超过一定阈值时,则触发动态扩容
Java中的哈希表:
HashMap:线程不安全
HashTable:线程安全
ConcurrentHashMap:支持高并发的哈希表
代码实现哈希表
class HashTableDef {
private EmpLinkedList[] empLinkedListsArray;
private int size;
//初始化empLinkedListsArray
public HashTableDef(int size) {
this.size = size;
empLinkedListsArray = new EmpLinkedList[this.size];
//初始化每一条链表,否则empLinkedListsArray中的每条链表皆为null
for (int i = 0; i < size; i++)
empLinkedListsArray[i] = new EmpLinkedList();
}
//用去模法,编写散列函数,根据id求应该添加到哪条链表
public int hashFunc(int id) {
return id % size;
}
//添加员工
public void add(Emp emp) {
//根据员工id,确定应添加至哪条链表
int addedLinkedListNum = hashFunc(emp.id);
empLinkedListsArray[addedLinkedListNum].add(emp);
}
//遍历哈希表,即遍历所有链表
public void list() {
for (int i = 0; i < size; i++)
empLinkedListsArray[i].list(i);
}
//根据id查找员工
public void findEmpById(int id) {
//使用散列函数确定在哪一条链表中寻找
int targetLinkedListNum = hashFunc(id);
Emp targetEmp = empLinkedListsArray[targetLinkedListNum].findEmpById(id);
if (targetEmp != null) {
System.out.println("在第" + (targetLinkedListNum + 1) + "条链表中找到id为" + id + "的员工");
} else {
System.out.println("未找到相应id的员工");
}
}
}
//员工类
class Emp {
public int id;
public String name;
public Emp next;
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
//链表
class EmpLinkedList {
//这里的head亦代表一个员工,而非仅用于管理链表
private Emp head;//默认null
//假定添加员工时,id自增,即id的分配总是从小而大
//故直接将员工添加至链表尾部即可
public void add(Emp emp) {
//添加第一个员工时
if (head == null) {
head = emp;
return;
}
//并非添加第一个员工时
//用指针寻找链表最后一个结点
Emp ptr = head;
while (true) {
if (ptr.next == null) break;
ptr = ptr.next;
}
//将emp添加至链表尾部
ptr.next = emp;
}
//遍历链表信息
public void list(int num) {
//链表为空
if (head == null) {
System.out.println("第" + (num + 1) + "条链表为空");
return;
}
System.out.println("第" + (num + 1) + "条链表信息为");
Emp ptr = head;
while (true) {
if (ptr == null) break;
System.out.println(ptr);
ptr = ptr.next;
}
}
//根据id查找员工
public Emp findEmpById(int id) {
//判断链表为空与否
if (head == null) {
System.out.println("链表为空");
return null;
}
Emp ptr = head;
while (true) {
if (ptr == null) break;
if (ptr.id == id) break;
ptr = ptr.next;
}
return ptr;
}
}
树(Tree)
数组的优缺点:
可以通过下标快速访问数据,有序数组还可以通过二分查找等算法提高查找性能;插入和删除元素时需要移动其他元素,性能较低;数组定义的大小大于实际需要的大小,可能会浪费空间,如果大小不足,则需要重新分配更大的空间并复制数据
而链表的增删,无需移动其他元素,效率较高,但查找时需要从头至尾遍历,性能低
基本概念
而树增删改查的性能都不错
非线性数据结构
根结点(Root Node)
子结点(Child Node)
父结点(Parent Node)
叶子结点(Leaf Node):无子结点
结点的度(Degree):一个结点的子结点个数
树的度(Degree of Tree):所有结点的最大度数
层级(Level):根结点为第0层,以此类推
路径(Path):两结点间的连接关系
深度(Depth):某结点到根结点的路径长度
高度(Height):根节点到叶子结点的最长路径长度(最大层数/最大深度)
子树(Subtree)
森林(Forest):若干互不相交的树组成的集合
分类
普通树(General Tree):
每个结点可以有任意数量的子结点,如文件系统
二叉树(Binary Tree):
每个结点最多有两个结点,左子节点和右子节点
完全二叉树(Compele Bianry Tree):
除最后一层,其他层节点数必须达到最大值,最后一层如果不满,则结点必须从左至右排列,不能出现空隙
满二叉树(Full Binary Tree)
叶子结点均在最后一层,非叶子结点都有两个子节点anyway 顾名思义,满!!
二叉树(Binary Tree)
三种遍历
前序遍历(Pre-order Traversal)
根左右
中序遍历(In-order Traversal)
左根右
后序遍历(Post-order Traversal)
左右根
class BinaryTree {
//根节点
private Node root;
public void setRoot(Node root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
}else {
System.out.println("二叉树为空");
}
}
//中序遍历
public void inOrder() {
if (this.root != null) {
this.root.inOrder();
}else {
System.out.println("二叉树为空");
}
}
//后序遍历
public void postOrder() {
if (this.root != null) {
this.root.postOrder();
}else {
System.out.println("二叉树为空");
}
}
}
class Node {
private int id;
private String name;
private Node left;
private Node right;
//对结点的前序遍历
public void preOrder() {
//输出当前节点
System.out.println(this);
//若左子树不为空,则递归左子树
if (this.left != null)
this.left.preOrder();
//若右子树不为空,则递归右子树
if (this.right != null)
this.right.preOrder();
}
//对结点的中序遍历
public void inOrder() {
//若左子树不为空,则递归左子树
if (this.left != null)
this.left.inOrder();
//输出当前结点
System.out.println(this);
//若右子树不为空,则递归右子树
if (this.right != null)
this.right.inOrder();
}
//对结点的后序遍历
public void postOrder() {
//若左子树不为空,则递归左子树
if (this.left != null)
this.left.postOrder();
//若右子树不为空,则递归右子树
if (this.right != null)
this.right.postOrder();
//输出当前节点
System.out.println(this);
}
public Node(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Node getLeft() {
return left;
}
public void setLeft(Node left) {
this.left = left;
}
public Node getRight() {
return right;
}
public void setRight(Node right) {
this.right = right;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

1607

被折叠的 条评论
为什么被折叠?



