数据data结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。学好数据结构可以编写出更加漂亮,更加有效率的代码。
程序 = 数据结构 + 算法
一、数据结构
数据结构包括:线性结构和非线性结构。
线性结构:
线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系
线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。
- 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据
- 元素以及相邻元素的地址信息
线性结构常见的有:数组、队列、链表和栈
非线性结构:二维数组,多维数组,广义表,树结构,图结构
1. 数组
1.1 稀疏数组
实际需求:编写五子棋程序时,有存盘和续上盘的功能要求
因为该二维数组的很多值是默认值0, 因此记录了很多没有意义的数据。> 稀疏数组。
基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
稀疏数组举例说明
应用实例
- 使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等) (如果直接存盘,class.data文件很大,尤其是大的二维数组(比如:地图或者棋盘),所以采用稀疏数组进行优化。)
- 把稀疏数组存盘,并且可以从新恢复原来的二维数组数
- 代码实现
public class SparseArray {
public static void main(String[] args) {
int[][] sourceArr = new int[6][7];
sourceArr[0][3] = 22;
sourceArr[0][6] = 15;
sourceArr[1][1] = 11;
sourceArr[1][5] = 17;
sourceArr[2][3] = -6;
sourceArr[3][5] = 39;
sourceArr[4][0] = 91;
sourceArr[5][2] = 28;
System.out.println("==========原始数组为==========");
for (int i = 0; i < sourceArr.length; i++) {
for (int j = 0; j < sourceArr[i].length; j++) {
System.out.print(sourceArr[i][j] + "\t");
}
System.out.println();
}
// 统计原始数组中非0 数据:
int count = 0;
for (int i = 0; i < sourceArr.length; i++) {
for (int j = 0; j < sourceArr[i].length; j++) {
if (sourceArr[i][j] != 0){
count++;
}
}
}
// 创建稀疏数组:
int[][] sparseArray = new int[count + 1][3];
int row = 1;
sparseArray[0][0] = sourceArr.length;
sparseArray[0][1] = sourceArr[0].length;
sparseArray[0][2] = count;
for (int i = 0; i < sourceArr.length; i++) {
for (int j = 0; j < sourceArr[i].length; j++) {
if (sourceArr[i][j] != 0){
sparseArray[row][0] = i;
sparseArray[row][1] = j;
sparseArray[row][2] = sourceArr[i][j];
row++;
}
}
}
System.out.println("==========稀疏数组为==========");
for (int i = 0; i < sparseArray.length; i++) {
for (int j = 0; j < sparseArray[i].length; j++) {
System.out.print(sparseArray[i][j] + "\t");
}
System.out.println();
}
// 还原为原始数组:
int[][] aimArray = new int[sparseArray[0][0]][sparseArray[0][1]];
for (int i = 1; i < sparseArray.length; i++) {
aimArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
System.out.println("==========还原后的原始数组为==========");
for (int i = 0; i < aimArray.length; i++) {
for (int j = 0; j < aimArray[i].length; j++) {
System.out.print(aimArray[i][j] + "\t");
}
System.out.println();
}
}
}
2 队列
应用场景:银行排队
队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出
示意图:(使用数组模拟队列示意图)
2.1 数组模拟单向队列
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:
当将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:
(1)将尾指针往后移:rear+1 , 当front == rear 【空】
(2)若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]
注:1. rear 是队列最后[含] 2. front 是队列最前元素[不含]
队列具备的功能:
- 出队列操作getQueue
- 显示队列的情况showQueue
- 查看队列头元素headQueue
- 退出系统exit
- 查看队列头元素headQueue
- 显示队列的情况showQueue
public class Queue {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
CreateQueue queue = new CreateQueue(3);
boolean flag = true;
while (flag){
// System.out.println("========================");
System.out.println("请给出指令:");
// System.out.println("========================");
String key = scanner.nextLine();
switch (key){
case "get":
queue.getNum();
break;
case "add":
System.out.println("请输入要加入的数据");
int num = scanner.nextInt();
queue.add(num);
break;
case "show":
queue.show();
break;
case "head":
queue.head();
break;
case "exit":
scanner.close();
flag = false;
break;
default:
break;
}
}
}
}
class CreateQueue{
int maxsize;
int front;
int rear;
int[] queue;
public CreateQueue(int maxsize) {
this.maxsize = maxsize;
front = -1;
rear = -1;
queue = new int[maxsize];
}
public boolean isFull(){
return rear == maxsize - 1;
}
public boolean isEmpty(){
return front == rear;
}
public void add(int num){
if (isFull()){
System.out.println("队列已满,无法加入");
return;
}
rear++;
queue[rear] = num;
}
public void getNum(){
if (isEmpty()){
System.out.println("队列已空,无法取出。");
return;
}
System.out.println("取出" + queue[front+1]);
front++;
}
public void head(){
if (isEmpty()){
System.out.println("队列空,无数据!");
return;
}
front++;
System.out.println("队列头数据为:" + queue[front]);
}
public void show(){
if (isEmpty()){
System.out.println("队列为空!");
return;
}
for (int i = front + 1; i < rear +1; i++) {
System.out.println("第" + i + "个位置元素为:" + queue[i]);
}
}
}
问题分析并优化:
1)目前数组使用一次就不能再用,没有达到复用的效果
2)将这个数组改成环形的队列,取模:%
2.2 数组模拟环形队列
对前面的数组模拟队列的优化,充分利用数组.因此将数组看做是一个环形的。(通过取模的方式来实现即可)
分析说明:
1)尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个在做判断队列满的时候需要注意 (rear + 1) % maxSize == front 满]
2)rear == front [空]
3)测试示意图:
- front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素,front的初始值为0;
- rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定,rear的初始值为0;
- 当队列满时,条件是(rear+1)%maxSize = front 【满】;
- 当队列为空的条件,rear == front 【空】;
- 队列中有效的数据的个数 (rear + maxSize - front) % maxSize //rear = 1 front = 0;
public class CircleQueue {
public static void main(String[] args) {
CreateCircleQueue queue = new CreateCircleQueue(4);
Scanner scanner = new Scanner(System.in);
boolean flag = true;
String key;
while (flag){
System.out.println("请输入指令:");
key = scanner.nextLine();
switch (key){
case "show":
queue.show();
break;
case "add":
int num = scanner.nextInt();
queue.add(num);
break;
case "get":
try {
int get = queue.getQueue();
System.out.println(get);
} catch (Exception e) {
e.printStackTrace();
}
break;
case "head":
try {
int head = queue.getHead();
System.out.println(head);
} catch (Exception e) {
e.printStackTrace();
}
break;
case "exit":
scanner.close();
flag = false;
break;
}
}
}
}
class CreateCircleQueue{
int maxsize;
int front;
int rear;
int[] queue;
public CreateCircleQueue(int maxsize) {
this.maxsize = maxsize;
front = 0;
rear = 0;
queue = new int[maxsize];
}
public boolean isEmpty(){
return rear == front;
}
public boolean isFull(){
return (rear + 1 - front ) % maxsize == 0;
}
public void add(int num){
if (isFull()){
System.out.println("队列已满,无法加入");
return;
}
queue[rear] = num;
rear = (rear + 1)% maxsize;
}
public int getQueue(){
if (isEmpty()){
throw new RuntimeException("队列为空,无法取出。");
}
int temp = queue[front];
front = (front+1) % maxsize;
return temp;
}
public int getHead(){
if (isEmpty()){
throw new RuntimeException("队列为空,无法得到头数据!");
}
return queue[front];
}
public int size(){
return (rear + maxsize - front) % maxsize;
}
public void show(){
if (isEmpty()){
System.out.println("队列为空");
return;
}
for (int i = front; i < front + size(); i++) {
System.out.println("第" + i + "个元素为:" + queue[i % maxsize]);
}
}
}
3. 链表
链表是有序的列表,但是它在内存中是存储如下
小结:
1)链表是以节点的方式来存储,是链式存储
2)每个节点包含 data 域, next 域:指向下一个节点.
3)如图:发现链表的各个节点不一定是连续存储.
4)链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
3.1 单链表
单链表(带头结点) 逻辑结构示意图如下
使用带head头的单向链表完成增删改查操作:
(头结点:① 无具体数据;② 表示单链表头)
第一种方法在添加节点时,直接添加到链表的尾部
添加辅助变量遍历是因为head节点不能动,只能动辅助结点。
第二种方式在添加节点时,根据序号大小将节点插入到指定位置(如果有这个序号,则添加失败,并给出提示)
删除功能点:
单链表的常见面试题有如下:
![](https://i-blog.csdnimg.cn/blog_migrate/4d5c8696d8bc10d2e090a56c8ecd98b9.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a1c0f20a2eee76cee4e7496877370566.png)
步骤2-1:将指针从当前节点移向下一节点;
步骤2-2:将当前节点的下一节点(temp.next)指向新链表最前端;
步骤2-3:将当前节点连接到新链表头结点reverseHead后(新链表头结点的next指向当前节点),形成新链表。
![](https://i-blog.csdnimg.cn/blog_migrate/d2db551c7afb0e9f5d6266b9481779bb.png)
代码实现:
public class SingleLinkedList {
public static void main(String[] args) {
Node1 node1 = new Node1(1, "唐僧");
Node1 node2 = new Node1(2, "孙悟空");
Node1 node3 = new Node1(3, "猪八戒");
Node1 node4 = new Node1(4, "沙僧");
Node1 node5 = new Node1(5, "白龙马");
SingleList singleList = new SingleList();
// singleList.add(node1);
// singleList.add(node2);
// singleList.add(node4);
// singleList.add(node5);
singleList.addByOrd(node3);
singleList.addByOrd(node1);
singleList.addByOrd(node5);
singleList.addByOrd(node4);
singleList.addByOrd(node2);
node3 = new Node1(3, "悟能");
singleList.update(node3);
singleList.list();
System.out.println("====================");
// singleList.del(2);
// singleList.list();
System.out.println(cnt(singleList.getHead()));
System.out.println("=======================");
reverse(singleList.getHead());
singleList.list();
// Node1 node = getNode(2, singleList.getHead());
// System.out.println(node);
}
public static int cnt(Node1 head){
int count = 0;
Node1 temp = head;
while (true){
if (temp.next == null){
break;
}
temp = temp.next;
count++;
}
return count;
}
public static Node1 getNode(int k, Node1 head){
Node1 temp = head;
// Node1 temp2 = head;
int count = 0;
while (true){
if (count == cnt(head) - k + 1){
break;
}
count++;
temp = temp.next;
// if (count >= k){
// temp2 = temp2.next;
// }
}
return temp;
}
public static void reverse(Node1 head){
Node1 reHead = new Node1(0, null);
Node1 temp = head.next;
Node1 cur;
while (temp != null){
cur = temp.next;
temp.next = reHead.next;
reHead.next = temp;
temp = cur;
}
head.next = reHead.next;
}
}
class Node1{
int no;
String name;
Node1 next;
public Node1(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "Node1{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
class SingleList{
Node1 head = new Node1(0, null);
public Node1 getHead() {
return head;
}
public void add(Node1 node){
if (head.next == null){
head.next = node;
}else {
Node1 temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = node;
}
}
public void addByOrd(Node1 node){
Node1 temp = head;
boolean flag = false;
while (true){
if (temp.next == null){
flag = true;
break;
}
if (node.no < temp.next.no){
flag = true;
break;
}
temp = temp.next;
}
if (flag){
node.next = temp.next;
temp.next = node;
}
}
// 改
public void update(Node1 node){
Node1 temp = head;
while (true){
if (temp.next == null){
break;
}
if (temp.next.no == node.no){
// node.next = temp.next.next;
// temp.next = node;
temp.next.name = node.name;
break;
}
temp = temp.next;
}
}
// 删除
public void del(int no){
Node1 temp = head;
if (temp.next == null){
System.out.println("空链表");
return;
}
while (true){
if (temp.next == null){
break;
}
if (temp.next.no == no){
temp.next = temp.next.next;
}
temp = temp.next;
}
}
public void list(){
if (head.next == null){
System.out.println("空链表");
return;
}
Node1 temp = head.next;
while (true){
if (temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
3.2 双向链表
单向链表的缺点分析:
删除的步骤4:
有一个前提条件:
if (temp.next != null) {temp.next.pre = temp.pre}
public class DoubleLinkedList {
public static void main(String[] args) {
Node node1 = new Node(1, "唐僧");
Node node2 = new Node(2, "孙悟空");
Node node3 = new Node(3, "猪八戒");
Node node4 = new Node(4, "沙僧");
Node node5 = new Node(5, "白龙马");
DoubleList doubleList = new DoubleList();
doubleList.add(node1);
doubleList.add(node2);
doubleList.add(node3);
doubleList.add(node4);
doubleList.add(node5);
doubleList.list();
System.out.println("===========================");
node3 = new Node(3, "悟净");
doubleList.update(node3);
doubleList.list();
System.out.println("===========================");
doubleList.del(5);
doubleList.list();
}
}
class Node{
int no;
String name;
Node next;
Node pre;
public Node(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
class DoubleList{
Node head = new Node(0, null);
public void add(Node node){
Node temp = head;
while (true){
if (temp.next == null){
break;
}
temp = temp.next;
}
temp.next = node;
node.pre = temp;
}
public void del(int no){
Node temp = head;
boolean flag = false;
while (true){
if (temp.no == no){
flag = true;
break;
}
if (temp.next == null){
break;
}
temp = temp.next;
}
if (flag){
temp.pre.next = temp.next;
if (temp.next != null) {
temp.next.pre = temp.pre;
}
}else {
System.out.println("不存在该节点");
}
}
public void update(Node node){
Node temp = head;
while (true){
if (temp.next == null){
break;
}
if (temp.next.no == node.no){
temp.next.name = node.name;
break;
}
temp = temp.next;
}
}
public void list(){
Node temp = head.next;
while (true){
if (temp == null){
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
3.3 约瑟夫单向环形链表
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
创建环形链表:
节点出圈:
环形链表出圈:
有num个结点,从start位置的节点开始,每隔count个的节点被取出;
- 创建一个辅助指针helper指向环形链表的最后节点;
- 取count为的节点前,先让first和helper移动(start-1)次,即:移动到开始的前一个位置;
- 开始数count次时,让first和helper指针同时移动(count-1)次;
- 将first指向的节点出圈;
代码实现:
public class CircleLinkedList {
public static void main(String[] args) {
CircleList circleList = new CircleList();
circleList.add(10);
circleList.show();
System.out.println();
// circleList.outQueueGame(5, 3);
System.out.println("========================");
circleList.getOut(3, 5);
}
}
class CircleNode{
int no;
CircleNode next;
public CircleNode(int no) {
this.no = no;
}
@Override
public String toString() {
return "CircleNode{" +
"no=" + no +
'}';
}
}
class CircleList{
CircleNode first;
public void add(int num){
CircleNode temp = first;
for (int i = 1; i <= num; i++) {
CircleNode node = new CircleNode(i);
if (i == 1){
first = node;
node.next = first;
temp = first;
}else {
temp.next = node;
node.next = first;
temp = node;
}
}
}
public void show(){
CircleNode temp = first;
while (true){
System.out.println(temp);
if (temp.next == first){
break;
}
temp = temp.next;
}
}
public void outQueueGame(int gap, int start){
CircleNode temp = first;
while (true){
if (temp.next == first){
break;
}
temp = temp.next;
}
for (int i = 0; i < start - 1; i++) {
first = first.next;
temp = temp.next;
}
while (true) {
if (temp == first){
break;
}
for (int i = 0; i < gap - 1; i++) {
first = first.next;
temp = temp.next;
}
System.out.print(first.no + "\t");
first = first.next;
temp.next = first;
}
System.out.println("遗留下的节点为:" + first.no);
}
public void getOut(int start, int gap){
CircleNode helper = first;
while (true){
if (helper.next == first){
break;
}
helper = helper.next;
}
for (int i = 0; i < start - 1; i++) {
first = first.next;
helper = helper.next;
}
while (true){
if (helper == first){
break;
}
for (int i = 0; i < gap - 1; i++) {
first = first.next;
helper = helper.next;
}
System.out.print(first.no + "\t");
first = first.next;
helper.next = first;
}
System.out.println("最后的节点为:" + first.no);
}
}
4. 栈
栈的实际需求:请输入一个表达式
计算式:[7*2*2-5+1-5+3-3] 点击计算【如下图】
请问: 计算机底层是如何运算得到结果的? 注意不是简单的把算式列出运算,因为我们看这个算式 7 * 2 * 2 - 5, 但是计算机怎么理解这个算式的(对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。-> 栈
栈的介绍:
栈的应用场景:
4.1 数组模拟栈
![](https://i-blog.csdnimg.cn/blog_migrate/ff9a3823c66308c1336f9448e0ec49d4.png)
![](https://i-blog.csdnimg.cn/blog_migrate/22cc83b03d794bba9930425d3ab7cc2d.png)
public class StackDemo {
public static void main(String[] args) {
Stack stack = new Stack(4);
Scanner scanner = new Scanner(System.in);
String key;
boolean flag = true;
while (flag){
System.out.println("请输入指令");
key = scanner.next();
switch (key){
case "push":
System.out.println("请输入入栈数字:");
int num = scanner.nextInt();
stack.push(num);
break;
case "pop":
int pop = stack.pop();
System.out.println("栈顶取出的元素为:" + pop);
break;
case "peek":
int peek = stack.peek();
System.out.println("栈顶元素为:" + peek);
break;
case "list":
stack.list();
break;
case "exit":
scanner.close();
flag = false;
break;
}
}
}
}
class Stack{
int[] arr;
int maxsize;
int top;
public Stack(int maxsize) {
this.maxsize = maxsize;
arr = new int[maxsize];
this.top = -1;
}
public void push(int num){
if (top == maxsize - 1){
System.out.println("栈已满,无法加入");
return;
}
top++;
arr[top] = num;
}
public int pop(){
if (top == -1){
throw new RuntimeException("栈已空,无法取出。");
}
int temp = arr[top];
top--;
return temp;
}
public int peek(){
if (top == -1){
throw new RuntimeException("栈已经空,无头元素。");
}
return arr[top];
}
public void list(){
if (top == -1){
System.out.println("空栈");
return;
}
for (int i = top; i >= 0; i--) {
System.out.println("第 " + i + " 个位置为:" + arr[i]);
}
}
}
4.2 栈的应用
(1)中缀表达式(综合计算器)
中缀表达式:
- 通过一个index值来遍历表达式;
- 如果是数字,直接入栈(通过拼接字符串转为数字);
- str += ch;
- 扫描到了最后一位(index == expression.length() - 1),直接压入数字栈;
- 未扫描到最后一位时,如果下一个符号是运算符,直接压入数字栈,并将数字串str置空;
- 如果扫描到的是一个符号
- 若当前符号栈为空,这直接进符号栈;
- 若符号栈有操作符,则进行比较:
- 若当前操作符优先级 <= 栈顶操作符优先级:需要从数字栈取出两个数,并从符号栈取出一个操作符进行运算,将得到的结果入数栈,当前扫描到的符号入符号栈。
- 若当前操作符优先级 > 栈中操作符优先级:直接将该符号入符号栈。
- 当表达式扫描完(index >= expression.length() ),顺序从数栈和符号栈中取出相应的数字和符号,并进行计算。符号栈为空时完成计算。
- 最后栈中只有一个数字,即表达式结果。
代码实现:
public class GenaralExpression {
public static void main(String[] args) {
String exp = "12/3*4+18-12+3"; //中缀表达式
Stack<Character> operStack = new Stack<>();
Stack<Integer> numStack = new Stack<>();
int index = 0;
String num = "";
while (index < exp.length()){
char c = exp.charAt(index);
if ((c + "").matches("[0-9]")){
num += c;
if (index == exp.length() - 1){
numStack.push(Integer.parseInt(num));
}else {
char ch = exp.charAt(index +1);
if ((ch + "").matches("[^0-9]")){
numStack.push(Integer.parseInt(num));
num = "";
}
}
}else {
if (operStack.size() == 0){
operStack.push(c);
}else {
if (prio(c) <= prio(operStack.peek())){
int num2 = numStack.pop();
int num1 = numStack.pop();
Character pop = operStack.pop();
int res = cal(num1, num2, pop);
numStack.push(res);
operStack.push(c); ///
}else {
operStack.push(c);
}
}
}
index++;
}
while (operStack.size() >0){
int num2 = numStack.pop();
int num1 = numStack.pop();
Character pop = operStack.pop();
int res = cal(num2, num1, pop);
numStack.push(res);
}
System.out.println(numStack.peek());
}
public static int prio(char ope){
if (ope == '+' || ope == '-'){
return 1;
}else if (ope == '/' || ope == '*'){
return 2;
}else {
return 0;
}
}
public static int cal(int num1, int num2, char ope){
int res;
if (ope == '+'){
res = num1 + num2;
}else if (ope == '-'){
res = num1 - num2;
} else if (ope == '*') {
res = num1 * num2;
}else {
return num1 / num2;
}
return res;
}
}
(2)前缀表达式
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
1)从右至左扫描,将6、5、4、3压入堆栈
2)遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
3)接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
4)最后是-运算符,计算出35-6的值,即29,由此得出最终结果
(3)后缀的计算
后缀表达式
正常的表达式 | 逆波兰表达式 |
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 + = |
后缀表达式的计算机求值
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
(4)中缀转后缀表达式
后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。
具体步骤如下:
4)中的三步可以简化成:
While(s1.size() != 0 && 当前扫描到的运算符优先级 <= s1栈顶运算符){s2.add(s1.pop()); s1.push(item);} 其他直接压入s1;
(1) 如果是左括号“ (” ,则直接压入 s1
(2) 如果是右括号“ )” ,则依次弹出 s1 栈顶的运算符,并压入 s2 ,直到遇到左括号为止,此时将这一对括号丢弃
举例说明:
将中缀表达式“1+((2+3)×4)-5”转换为后缀表达式的过程如下
扫描到的元素 | s2(栈底->栈顶) | s1 (栈底->栈顶) | 说明 |
1 | 1 | 空 | 数字,直接入栈 |
+ | 1 | + | s1为空,运算符直接入栈 |
( | 1 | + ( | 左括号,直接入栈 |
( | 1 | + ( ( | 同上 |
2 | 1 2 | + ( ( | 数字 |
+ | 1 2 | + ( ( + | s1栈顶为左括号,运算符直接入栈 |
3 | 1 2 3 | + ( ( + | 数字 |
) | 1 2 3 + | + ( | 右括号,弹出运算符直至遇到左括号 |
× | 1 2 3 + | + ( × | s1栈顶为左括号,运算符直接入栈 |
4 | 1 2 3 + 4 | + ( × | 数字 |
) | 1 2 3 + 4 × | + | 右括号,弹出运算符直至遇到左括号 |
- | 1 2 3 + 4 × + | - | -与+优先级相同,因此弹出+,再压入- |
5 | 1 2 3 + 4 × + 5 | - | 数字 |
到达最右端 | 1 2 3 + 4 × + 5 - | 空 | s1中剩余的运算符 |
因此结果为 "1 2 3 + 4 × + 5 –"
代码实现:
public class Poland {
public static void main(String[] args) {
String exp = "0-3+2*(1+2*(0-4/(8-6)+7))";//"1+((2+3)*4)-5"; //"12/3*4+18-12+33/11*2"; //28
List<String> srcList = getExpList(exp);
System.out.println(srcList);
Stack<String> opStack = new Stack<>();
ArrayList<String> list = new ArrayList<>();
for (String s : srcList) {
if (s.matches("\\d+")){
list.add(s);
}else {
if (s.equals("(")) {
opStack.push(s);
} else if (s.equals(")")) {
while (!opStack.peek().equals("(")) {
list.add(opStack.pop());
}
opStack.pop();
} else {
while (opStack.size() != 0 && prio(s) <= prio(opStack.peek())) {
list.add(opStack.pop());
}
opStack.push(s);
}
}
}
while (opStack.size() > 0){
list.add(opStack.pop());
}
System.out.println(opStack);
System.out.println(list);
Stack<Integer> stack = new Stack<>();
for (String s : list) {
if (s.matches("\\d+")){
stack.push(Integer.parseInt(s));
}else {
Integer num2 = stack.pop();
Integer num1 = stack.pop();
int res = cal(num1, num2, s);
stack.push(res);
}
}
System.out.println(stack.peek());
}
public static int cal(int num1, int num2, String ope){
int res;
if (ope.equals("+")){
res = num1 + num2;
}else if (ope.equals("-")){
res = num1 - num2;
} else if (ope.equals("*")) {
res = num1 * num2;
}else {
return num1 / num2;
}
return res;
}
public static boolean isOp(String op){
return op.equals("+") || op.equals("-") || op.equals("*") || op.equals("/");
}
public static int prio(String ope){
if (ope.equals("+") || ope.equals("-")){
return 1;
}else if (ope.equals("*") || ope.equals("/")){
return 2;
}else {
return 0;
}
}
public static List<String> getExpList(String str){
char[] chars = str.toCharArray();
String num = "";
int index = 0;
ArrayList<String> list = new ArrayList<>();
while (index < str.length()) {
char c = str.charAt(index);
if ((c + "").matches("[0-9]")){
num += c;
if (index == str.length() - 1){
list.add(num);
}else {
if ((str.charAt(index + 1) + "").matches("[^0-9]")){
list.add(num);
num = "";
}
}
}else {
list.add(c+"");
}
index++;
}
return list;
}
}
(5)完整版逆波兰计算器
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();
}
}
}
5. 哈希表(散列表)
例题:有一个公司,当有新的员工来报道时,要求将该员工的信息加入(id,性别,年龄,住址..),当输入该员工的id时,要求查找到该员工的 所有信息.
要求: 不使用数据库,尽量节省内存,速度越快越好=>哈希表(散列)
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
添加时,保证按照id从低到高插入, 使用链表来实现哈希表, 该链表不带表头 [即: 链表的第一个结点就存放雇员信息]
代码实现:
public class HashTab {
public static void main(String[] args) {
HashTabLinkedList hashTabLinkedList = new HashTabLinkedList(6);
Scanner scanner = new Scanner(System.in);
String key;
while (true){
System.out.println("请输入指令:");
key = scanner.next();
switch (key){
case "add":
System.out.println("请输入id:");
int id = scanner.nextInt();
System.out.println("请输入name:");
String name = scanner.next();
hashTabLinkedList.add(new Emp(id, name));
break;
case "list":
hashTabLinkedList.list();
break;
case "emp":
System.out.println("请输入要查询的id:");
int no = scanner.nextInt();
hashTabLinkedList.empsById(no);
break;
case "exit":
scanner.close();
System.exit(0);
break;
}
}
}
}
class HashTabLinkedList{
EmpLinkedList[] empArrays;
int size;
public HashTabLinkedList(int size) {
this.size = size;
empArrays = new EmpLinkedList[size];
for (int i = 0; i < size; i++) {
empArrays[i] = new EmpLinkedList(); /
}
}
public void add(Emp emp){
empArrays[linkedListNo(emp.no)].add(emp);
}
public void list(){
for (int i = 0; i < size; i++) {
System.out.print("链表" + (i + 1) + "为:");
empArrays[i].list(i);
System.out.println();
}
}
public void empsById(int no){
Emp emp = empArrays[linkedListNo(no)].empById(no);
if (emp == null){
System.out.println("在哈希表中,未找到该雇员");
}else {
System.out.println("在第" + linkedListNo(no) + "条链表找到:" + emp);
}
}
public int linkedListNo(int no){
return no % size;
}
}
class EmpLinkedList{
Emp head;
public void add(Emp emp){
Emp temp = head;
if (head == null){
head = emp;
return;
}
while (true){
if (temp.next == null){
break;
}
temp = temp.next;
}
temp.next = emp;
}
public void list(int no){
if (head == null){
System.out.println("链表" + ( no + 1) + "为空");
return;
}
Emp temp = head;
while (true){
System.out.print("-->" + temp);
if (temp.next == null){
break;
}
temp = temp.next;
}
}
public Emp empById(int no){
if (head == null){
throw new RuntimeException("不存在该员工");
}
Emp temp = head;
while (true){
if (temp.no == no){
break;
}
if (temp.next == null){
temp = null;
break;
}
temp =temp.next;
}
return temp;
}
}
class Emp{
int no;
String name;
Emp next;
public Emp(int no, String name) {
this.no = no;
this.name = name;
}
@Override
public String toString() {
return "Emp{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
6. 树
为什么需要树这种数据结构
优点:通过下标方式访问元素,速度快。 对于有序数组 ,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值 ( 按一定顺序 ) 会整体移动 ,效率较低 [ 示意图 ]
![](https://i-blog.csdnimg.cn/blog_migrate/765270926dba70dc42bc6f2213e0a22f.png)
优点:在一定程度上对数组存储方式有优化 ( 比如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好 ) 。
缺点:在进行检索时,效率仍然较低,比如 ( 检索某个值,需要从头节点开始遍历 ) 【 示意图 】
![](https://i-blog.csdnimg.cn/blog_migrate/a95a4bf3e5ac137b14937e466080bed1.png)
能提高数据 存储,读取 的效率 , 比如利用 二叉排序树 (Binary Sort Tree) ,既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度 。
案例 : [7, 3, 10, 1, 5, 9, 12]
![](https://i-blog.csdnimg.cn/blog_migrate/a335e2267c40efb46de3faab2bf21bae.png)
树的常用术语(结合示意图理解):
6.1 二叉树
二叉树遍历的说明
使用前序,中序和后序对下面的二叉树进行遍历.
前序遍历: 先输出父节点,再遍历左子树和右子树
中序遍历: 先遍历左子树,再输出父节点,再遍历右子树
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
小结: 看输出父节点的顺序,就确定是前序,中序还是后序
二叉树-查找指定节点
二叉树-删除节点
如果删除的节点是叶子节点,则删除该节点
如果删除的节点是非叶子节点,则删除该子树.
代码实现以上功能点:
public class BinaryTree {
public static void main(String[] args) {
Node node1 = new Node(1);
Node node2 = new Node(2);
Node node3 = new Node(3);
Node node4 = new Node(4);
Node node5 = new Node(5);
Node node6 = new Node(6);
Node node7 = new Node(7);
node1.left = node2;
node1.right = node3;
node2.left = node4;
node2.right = node5;
node3.left = node6;
node3.right = node7;
BinaryTreeDemo binaryTreeDemo = new BinaryTreeDemo();
binaryTreeDemo.setRoot(node1);
binaryTreeDemo.preOrder();
System.out.println("====================");
binaryTreeDemo.infixOrder();
System.out.println("====================");
binaryTreeDemo.postOrder();
System.out.println("***********************");
System.out.println(binaryTreeDemo.preSearch(6));
System.out.println("-------------------------------");
binaryTreeDemo.del(2);
binaryTreeDemo.preOrder();
}
}
class BinaryTreeDemo{
Node root;
public void setRoot(Node root) {
this.root = root;
}
public void preOrder(){
if (root == null){
throw new RuntimeException("空树");
}else {
root.preOrder();
}
}
public void infixOrder(){
if (root == null){
throw new RuntimeException("空树");
}else {
root.infixOrder();
}
}
public void postOrder(){
if (root == null){
throw new RuntimeException("空树");
}else {
root.postOrder();
}
}
public Node preSearch(int no){
if (root == null){
System.out.println("空树");
return null;
}else {
return root.preSearch(no);
}
}
public void del(int no){
if (root != null) {
if (root.no == no) {
root = null;
} else {
root.del(no);
}
}else {
System.out.println("空树,不能删除~");
}
}
}
class Node{
int no;
Node left;
Node right;
public Node(int no) {
this.no = no;
}
@Override
public String toString() {
return "Node{" +
"no=" + no +
'}';
}
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
public void infixOrder(){
if (this.left != null){
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null){
this.right.infixOrder();
}
}
public void postOrder(){
if (this.left != null){
this.left.postOrder();
}
if (this.right != null){
this.right.postOrder();
}
System.out.println(this);
}
public Node preSearch(int no){
System.out.println("进入前序遍历");
Node temp = null;
if (this.no == no){
return this;
}
if (this.left != null){
temp = this.left.preSearch(no);
}
if (temp != null){
return temp;
}
if (this.right != null){
temp = this.right.preSearch(no);
}
return temp;
}
public void del(int no){
if (this.left != null && this.left.no == no){
this.left = null;
return;
}
if (this.right != null && this.right.no == no){
this.right = null;
return;
}
if (this.left != null){
this.left.del(no);
}
if (this.right != null){
this.right.del(no);
}
}
}
6.2 顺序存储二叉树
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组,看示意图。
要求:
顺序存储二叉树的特点:
完全二叉树:如果该二叉树的所有叶子结点都在最后一层或者倒数第二层,且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续
代码实现:
public class SeqBinaryTree {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
int[] arr1 = {};
SeqBinaryTreeDemo seqBinaryTreeDemo = new SeqBinaryTreeDemo(arr);
seqBinaryTreeDemo.preOrder();
}
}
class SeqBinaryTreeDemo{
int[] arr;
public SeqBinaryTreeDemo(int[] arr) {
this.arr = arr;
}
public void preOrder(){
preOrder(0);
}
public void preOrder(int index){
if (arr == null || arr.length == 0){
System.out.println("数组为空,无法遍历");
return;
}
System.out.println(arr[index]);
if ((2 * index + 1) < arr.length){
preOrder(2 * index + 1);
}
if ((2 * index + 2) < arr.length){
preOrder(2 * index + 2);
}
}
}
应用实例:堆排序(见6.4节)
6.3 线索化二叉树
将数列 {1, 3, 6, 8, 10, 14 } 构建成一颗二叉树. n+1=7
问题分析:
线索二叉树基本介绍
线索二叉树应用案例
应用案例说明:将下面的二叉树,进行中序线索二叉树。中序遍历的数列为 {8, 3, 10, 1, 14, 6}
说明: 当线索化二叉树后,Node节点的 属性 left 和 right ,有如下情况:
遍历线索化二叉树
说明:对前面的中序线索化的二叉树, 进行遍历
分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致。
代码实现:
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
Node2 node1 = new Node2(1);
Node2 node2 = new Node2(3);
Node2 node3 = new Node2(6);
Node2 node4 = new Node2(8);
Node2 node5 = new Node2(10);
Node2 node6 = new Node2(14);
node1.left = node2;
node1.right = node3;
node2.left = node4;
node2.right = node5;
node3.left = node6;
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree(node1);
threadedBinaryTree.threadedNodes();
threadedBinaryTree.threadedList();
}
}
class ThreadedBinaryTree{
Node2 root;
public ThreadedBinaryTree(Node2 root) {
this.root = root;
}
Node2 pre = null;
public void threadedNodes(){
this.threadedNodes(root);
}
public void threadedNodes(Node2 node){
if (node == null){
return;
}
threadedNodes(node.left);
if (node.left == null){
node.left = pre;
node.leftType = 1;
}
if (pre != null && pre.right == null){
pre.right = node;
pre.rightType = 1;
}
pre = node;
threadedNodes(node.right);
}
public void threadedList(){
Node2 node = root;
while (node != null){
while (node.leftType == 0){
node = node.left;
}
System.out.println(node);
if (node.rightType == 1){
node = node.right;
System.out.println(node);
}
node = node.right;
}
}
}
class Node2{
int no;
Node2 left;
Node2 right;
int leftType;
int rightType;
public Node2(int no) {
this.no = no;
}
@Override
public String toString() {
return "Node2{" +
"no=" + no +
'}';
}
}
6.4 树结构的实际应用
(1)堆排序
堆排序基本介绍
![](https://i-blog.csdnimg.cn/blog_migrate/afe893747996cc877f65f444fb8f08ef.png)
![](https://i-blog.csdnimg.cn/blog_migrate/de103c1b42d2bca5128a08e2e2426c2b.png)
大顶堆特点:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2] // i 对应第几个节点,i从0开始编号
![](https://i-blog.csdnimg.cn/blog_migrate/9d913e0f37c7ef60901856a7af6f8be9.png)
小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] // i 对应第几个节点,i从0开始编号
堆排序基本思想
堆排序的基本思想是:
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了.
堆排序步骤图解说明
要求:给一个数组{4,6,8,5,9},要求使用堆排序法,将数组升序排序
代码实现:
public class HeapSort {
public static void main(String[] args) {
int[] arr = {4,6,8,5,9};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr){
int temp = 0;
System.out.println("堆排序");
for (int i = arr.length / 2 - 1; i >= 0; i--) {
adjust(arr, i, arr.length);
}
for (int i = arr.length - 1; i > 0; i--) {
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
adjust(arr, 0, i);
}
}
public static void adjust(int[] arr, int i, int len){
int temp = arr[i];
for (int j = 2 * i + 1; j < len; j = 2 * j + 1) {
if (j + 1 < len && arr[j] < arr[j + 1]){
j++;
}
if (arr[j] > temp){
arr[i] = arr[j];
i = j;
}else {
break;
}
}
arr[i] = temp;
}
}
(2)赫夫曼树
基本介绍
赫夫曼树几个重要概念和举例说明
![](https://i-blog.csdnimg.cn/blog_migrate/e4c959572b40bae42a356a89133075ff.png)
赫夫曼树创建思路图解
给你一个数列 {13, 7, 8, 3, 29, 6, 1},要求转成一颗赫夫曼树.
思路分析(示意图):
构成赫夫曼树的步骤:
![](https://i-blog.csdnimg.cn/blog_migrate/8e4ccc42b01a40b9b4ae80458494c352.png)
public class HuffmanTreeDemo {
public static void main(String[] args) {
int[] arr = {13,7,8,3,29,6,1};
// createHuffman(arr);
preOrder(createHuffman(arr));
}
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}else {
System.out.println("空树。");
return;
}
}
public static Node createHuffman(int[] arr){
ArrayList<Node> nodes = new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
nodes.add(new Node(arr[i]));
}
// Collections.sort(nodes);
while (nodes.size() > 1){
Collections.sort(nodes);
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
Node newNode = new Node(leftNode.value + rightNode.value);
newNode.left = leftNode;
newNode.right = rightNode;
nodes.add(newNode);
nodes.remove(leftNode);
nodes.remove(rightNode);
}
return nodes.get(0);
}
}
class Node implements Comparable<Node>{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
@Override
public int compareTo(Node o) {
return this.value - o.value;
}
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
}
赫夫曼编码:
基本介绍
原理剖析
在线转码 工具 :https://www.mokuge.com/tool/asciito16/
说明:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小,比如 空格出现了 9 次, 编码为 0 , 其它依次类推 .
10 0 101 10 100 ...
原理剖析
通信领域中信息的处理方式3-赫夫曼编码
![](https://i-blog.csdnimg.cn/blog_migrate/eac2946338d147923fccf25364e81f66.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3d88560103c82f56e68515469b751fac.png)
4)根据赫夫曼树,给各个字符规定编码 , 向左的路径为0向右的路径为1 , 编码如下:
o: 1000 u: 10010 d: 100110 y: 100111 i: 101 a : 110 k: 1110 e: 1111 j: 0000 v: 0001 l: 001 : 01
5)按照上面的赫夫曼编码,我们的"i like like like java do you like a java" 字符串对应的编码为 (注意这里我们使用的无损压缩)
1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110
通过赫夫曼编码处理,长度为 : 133
说明:
赫夫曼编码是无损处理方案。
注意, 这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl 是一样的,都是最小的, 比如: 如果我们让每次生成的新的二叉树总是排在权值相同的二叉树的最后一个,则生成的二叉树为:
最佳实践-数据压缩(创建赫夫曼树)
将给出的一段文本,比如 "i like like like java do you like a java" , 根据前面的讲的赫夫曼编码原理,对其进行数据压缩处理 ,形式如 "1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110"
步骤1:根据赫夫曼编码压缩数据的原理,需要创建 "i like like like java do you like a java" 对应的赫夫曼树.
最佳实践-数据压缩(生成赫夫曼编码和赫夫曼编码后的数据)
已经生成了 赫夫曼树, 下面继续完成任务
=01 a=100 d=11000 u=11001 e=1110 v=11011 i =101 y=11010 j=0010 k=1111 l=000 o=0011
1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100
最佳实践-数据解压(使用赫夫曼编码解码)
使用赫夫曼编码来解码数据,具体要求是
, -57, 6, -24, -14, -117, -4, -60, -90, 28]
思路:解码过程,就是编码的一个逆向操作。
最佳实践-文件压缩
通过赫夫曼编码对一个字符串进行编码和解码, 下面完成对文件的压缩和解压, 具体要求:给你一个图片文件,要求对其进行无损压缩, 看看压缩效果如何。
思路:读取文件-> 得到赫夫曼编码表 -> 完成压缩
最佳实践-文件解压(文件恢复)
具体要求:将前面压缩的文件,重新恢复成原来的文件。
思路:读取压缩文件(数据和赫夫曼编码表)-> 完成解压(文件恢复)
赫夫曼编码压缩文件注意事项
代码实现:
public class HuffmanCode {
public static void main(String[] args) {
String content = "i like like like java do you like a java";
List<Node1> nodes = getNodes(content);
Node1 root = createHuffmanTree(nodes);
preOrder(root);
Map<Byte, String> huffmanCode = getHuffmanCode(root);
System.out.println(huffmanCode);
String codes = getCodes(huffmanCode, content);
System.out.println(codes);
byte[] huffmanByte = zip(codes);
System.out.println(Arrays.toString(huffmanByte));
byte[] bytes = deCode(huffmanByte, huffmanCode);
System.out.println(Arrays.toString(bytes));
System.out.println(new String(bytes));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
sb.append((char) bytes[i]);
}
System.out.println(sb.toString());
}
public static byte[] deCode(byte[] bytes, Map<Byte, String> huffmanCode){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
boolean flag = (i == bytes.length - 1);
stringBuilder.append(byteToBitString(b, !flag));
}
HashMap<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCode.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
ArrayList<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length();) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag){
String substring = stringBuilder.substring(i, i + count);
b = map.get(substring);
if (b == null){
count++;
}else {
flag = false;
}
}
list.add(b);
i = i + count;
}
byte[] bytes1 = new byte[list.size()];
int i = 0;
for (int j = 0; j < list.size(); j++) {
bytes1[i] = list.get(j);
i++;
}
return bytes1;
}
public static String byteToBitString(byte b, boolean flag){ //
int temp = b;
if (flag){
temp |= 256;
}
String str = Integer.toBinaryString(temp);
if (flag){
return str.substring(str.length() - 8);
}else {
return str;
}
}
// 封装一个整体的方法
public static byte[] zip(String huffmanCode){
int len = (huffmanCode.length() + 7) / 8;
byte[] bytes = new byte[len];
String substr = "";
int count = 0;
for (int i = 0; i < huffmanCode.length(); i += 8) {
substr = "";
if (i + 8 > huffmanCode.length()){
substr = huffmanCode.substring(i);
}else {
substr = huffmanCode.substring(i, i + 8);
}
bytes[count++] = (byte)Integer.parseInt(substr,2);
}
return bytes;
}
public static String getCodes(Map<Byte, String> huffmanCode, String str){
StringBuilder stringBuilder = new StringBuilder();
byte[] bytes = str.getBytes();
for (byte aByte : bytes) {
stringBuilder.append(huffmanCode.get(aByte));
}
return stringBuilder.toString();
}
public static Map<Byte, String> huffmanCode = new HashMap<>();
public static StringBuilder stringBuilder = new StringBuilder();
public static Map<Byte, String> getHuffmanCode(Node1 node){
if (node == null){
return null;
}
getHuffmanCode(node.left, "0", stringBuilder);
getHuffmanCode(node.right, "1", stringBuilder);
return huffmanCode;
}
public static Map<Byte, String> getHuffmanCode(Node1 node, String code, StringBuilder stringBuilder){
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code);
if (node != null){
if (node.data == null){
getHuffmanCode(node.left, "0", stringBuilder1);
getHuffmanCode(node.right, "1", stringBuilder1);
}else {
huffmanCode.put(node.data, stringBuilder1.toString());
}
}
return huffmanCode;
}
public static List<Node1> getNodes(String str){
ArrayList<Byte> bytes = new ArrayList<>();
for (int i = 0; i < str.length(); i++) {
bytes.add((byte)str.charAt(i));
}
HashMap<Byte, Integer> map = new HashMap<>();
for (Byte aByte : bytes) {
if (map.get(aByte) == null){
map.put(aByte, 1);
}else {
map.put(aByte, map.get(aByte) + 1);
}
}
ArrayList<Node1> nodes = new ArrayList<>();
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
nodes.add(new Node1(entry.getKey(), entry.getValue()));
}
return nodes;
}
public static Node1 createHuffmanTree(List<Node1> nodes){
while (nodes.size() > 1){
Collections.sort(nodes);
Node1 leftNode = nodes.get(0);
Node1 rightNode = nodes.get(1);
Node1 parent = new Node1(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
return nodes.get(0);
}
public static void preOrder(Node1 root){
if (root == null){
return;
}else {
root.preOrder();
}
}
}
class Node1 implements Comparable<Node1>{
Byte data;
int weight;
Node1 left;
Node1 right;
public Node1(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "Node1{" +
"data=" + data +
", weight=" + weight +
'}';
}
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
@Override
public int compareTo(Node1 o) {
return this.weight - o.weight;
}
}
(3)二叉排序树(BST)
先看一个需求
给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加。
解决方案分析
不管链表是否有序,查找速度都慢,添加数据速度比数组快,不需要数据整体移动。 [ 示意图 ]
二叉排序树介绍
二叉排序树:BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
特别说明:如果有相同的值,可以将该节点放在左子节点或右子节点
比如针对前面的数据 (7, 3, 10, 12, 5, 1, 9) ,对应的二叉排序树为:
二叉排序树创建和遍历
一个数组创建成对应的二叉排序树,并使用中序遍历二叉排序树,比如: 数组为 Array(7, 3, 10, 12, 5, 1, 9) , 创建成对应的二叉排序树为 :
二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑
删除的节点是叶子节点,即该节点下没有左右子节点。
比如这里的 ( 比如: 2, 5, 9, 12)
删除节点有一个子节点
删除的节点有一个子节点,即该节点有左子节点或者右子节点。比如这里的 (比如:1 )
删除的节点有两个子节点,即该节点有左子节点和右子节点。比如这里的 ( 比如: 7,3,10)
删除叶子节点:
- 先找到要删除的节点:target
- 找到target的父节点parent
- 判断target是parent的左/右子节点
- 是左子结点(parent.left = null);是右子节点(parent.right = null)
删除只有一棵子树的节点:
- 先找到要删除的节点target及其父节点parent;
- 确定target的子节点是左/右子节点;
- 确定target是parent的左/右子节点;
- 当target有左子节点:
- 若target是parent的左子结点:parent.left = target.left;
- 若target是parent的右子节点:parent.right = target.left;
- 若target有右子节点:
- 若target是parent的左子结点:parent.left = target.right;
- 若target是parent的右子节点:parent.right = target.right;
删除有两棵子树的节点:
- 找到要删除的节点target及其父节点parent;
- 从target的右子树找到最小节点(或从左子树找到最大节点)
- 用临时变量temp保存找到的该最大(小)值,并删除该节点。
- Target.value = temp;
代码实现:与下一节平衡二叉树一起
(4)平衡二叉树(AVL树/二叉搜索树)
看一个案例(说明二叉排序树可能的问题)
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树(BST), 并分析问题所在.
左边BST 存在的问题分析:
基本介绍
应用案例-单旋转(左旋转)
应用案例-单旋转(右旋转)
应用案例-双旋转
前面的两个数列,进行单旋转(即一次旋转)就可以将非平衡二叉树转成平衡二叉树,但是在某些情况下,单旋转不能完成平衡二叉树的转换。比如数列
int[] arr = { 10, 11, 7, 6, 8, 9 }; 运行原来的代码可以看到,并没有转成 AVL树.
int[] arr = {2,1,6,5,7,3}; // 运行原来的代码可以看到,并没有转成 AVL树
总结:
左旋:rightHeight() – leftHeight() > 1 时:
- 创建一个新节点,值为当前根节点的值;
- 新节点的左子树设为当前节点左子树;
- 新节点右子树设为当前节点右子树的左子树;
- 当前节点的值换为右子节点的值;
- 当前节点的右子树设置为右子树的右子树的值;
- 当前节点的左子树设为新节点;
右旋:与左旋相反
双旋:当前节点左子树的右子树高度>左子树高度;先对当前节点左子树进行左旋,再对当前节点右旋;
代码实现:
public class BinarySortTree {
public static void main(String[] args) {
// Nodes node1 = new Nodes(7);
// Nodes node2 = new Nodes(3);
// Nodes node3 = new Nodes(10);
// Nodes node4 = new Nodes(12);
// Nodes node5 = new Nodes(5);
// Nodes node6 = new Nodes(1);
// Nodes node7 = new Nodes(9);
//
// BinarySortTrees binarySortTrees = new BinarySortTrees(node1);
// binarySortTrees.add(node2);
// binarySortTrees.add(node3);
// binarySortTrees.add(node4);
// binarySortTrees.add(node5);
// binarySortTrees.add(node6);
// binarySortTrees.add(node7);
// binarySortTrees.infixOrder();
Nodes node1 = new Nodes(10);
Nodes node2 = new Nodes(11);
Nodes node3 = new Nodes(7);
Nodes node4 = new Nodes(6);
Nodes node5 = new Nodes(8);
Nodes node6 = new Nodes(9);
BinarySortTrees binarySortTrees = new BinarySortTrees();
binarySortTrees.add(node1);
binarySortTrees.add(node2);
binarySortTrees.add(node3);
binarySortTrees.add(node4);
binarySortTrees.add(node5);
binarySortTrees.add(node6);
System.out.println("++++++++++++++++++++++++++++++");
// System.out.println(binarySortTrees.getTarget(1));
// System.out.println(binarySortTrees.getTarget(5));
// System.out.println(binarySortTrees.getTarget(9));
// System.out.println("=================================");
// binarySortTrees.getParent(3);
// System.out.println(binarySortTrees.getParent(3));
// System.out.println(binarySortTrees.getParent(5));
// System.out.println(binarySortTrees.getParent(1));
// System.out.println(binarySortTrees.getParent(7));
// binarySortTrees.delNode(9);
binarySortTrees.infixOrder();
System.out.println(binarySortTrees.leftHeight(node1));
System.out.println(binarySortTrees.rightHeight(node1));
}
}
class BinarySortTrees{
Nodes root;
public BinarySortTrees() {
}
public BinarySortTrees(Nodes root) {
this.root = root;
}
public int leftHeight(Nodes nodes){
return nodes.leftHeight();
}
public int rightHeight(Nodes nodes){
return nodes.rightHeight();
}
public void add(Nodes node){
if (root == null){
root = node;
}else {
root.addNode(node);
}
}
public void infixOrder(){
if (root != null){
root.infixOrder();
}else {
return;
}
}
public Nodes getTarget(int value){
if (root != null){
return root.target(value);
}else {
return null;
}
}
public Nodes getParent(int value){
if (root != null){
return root.parent(value);
}else {
return null;
}
}
public int minRight(Nodes node){
Nodes target = node;
while (target.left != null){
target = target.left;
}
delNode(target.value);
return target.value;
}
public int maxLeft(Nodes node){
Nodes target = node;
while (target.right != null){
target = target.right;
}
delNode(target.value);
return target.value;
}
public void delNode(int value){
Nodes target = getTarget(value);
if (target == null){
System.out.println("不存在");
return;
}else {
if (root.left == null && root.right == null){
root = null;
}
Nodes parent = getParent(value);
if (target.left == null && target.right == null){
if (parent != null && parent.left.value == value){
parent.left = null;
}else if (parent != null && parent.right.value == value){
parent.right = null;
}
}else if (target.left != null && target.right != null){
int max = maxLeft(target.left);
// int min = minRight(target.right);
target.value = max;
}else {
if (target.left != null){
if (parent != null){
if (parent.left == target){
parent.left = target.left;
}else {
parent.right = target.left;
}
}else {
root = target.left;
}
}else if (target.right != null){
if (parent != null){
if (parent.left != null){
parent.left = target.right;
}else {
parent.right = target.right;
}
}else {
root = target.right;
}
}
}
}
}
}
class Nodes{
int value;
Nodes left;
Nodes right;
public Nodes(int value) {
this.value = value;
}
@Override
public String toString() {
return "Nodes{" +
"value=" + value +
'}';
}
public void addNode(Nodes node){
if (node == null){
return;
}
if (node.value < this.value){
if (this.left == null){
this.left = node;
}else {
this.left.addNode(node);
}
}else {
if (this.right == null){
this.right = node;
}else {
this.right.addNode(node);
}
}
if (this.leftHeight() - this.rightHeight() > 1){
if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){ /
this.left.leftRotate();
this.rightRotate();
}else {
this.rightRotate();///
}
return;
}
if (this.rightHeight() - this.leftHeight() > 1){
if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){ ///
this.right.rightRotate();
this.leftRotate();
}else {
this.leftRotate();/
}
}
}
public void leftRotate(){
Nodes newNode = new Nodes(this.value);
newNode.left = this.left;
newNode.right = this.right.left;
this.value = this.right.value;
this.right = this.right.right;
this.left = newNode;
}
public void rightRotate(){
Nodes newNode = new Nodes(this.value);
newNode.right = this.right;
newNode.left = this.left.right;
this.value = this.left.value;
this.left = this.left.left;
this.right = newNode;
}
public int leftHeight(){
if (this.left == null){
return 0; //
}
return this.left.height();
}
public int rightHeight(){
if (this.right == null){
return 0;
}
return this.right.height();
}
public int height(){
return Math.max(this.left == null?0: this.left.height(), this.right == null?0: this.right.height())+1;
}
public void infixOrder(){
if (this.left != null){
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null){
this.right.infixOrder();
}
}
public Nodes target(int value){
if (this.value == value){
return this;
}else if (this.left != null && value < this.value){
return this.left.target(value);
}else if (this.right != null && value > this.value){
return this.right.target(value);
}else {
return null;
}
}
public Nodes parent(int value){
if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)){
return this;
}else if (this.left != null && value < this.value){
return this.left.parent(value);
}else if (this.right != null && value > this.value){
return this.right.parent(value);
}else {
return null;
}
}
}
6.5 多路查找树
7. 图
为什么要有图
图的举例说明
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点。如图:
图的常用概念:
路径: 比如从 D -> C 的路径有 1) D->B->C 2) D->A->B->C
![](https://i-blog.csdnimg.cn/blog_migrate/e442183bf48718b867d81ed6b3fd6f29.png)
6) 带权图:这种边带权值的图也叫网.
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。
邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵是的row和col表示的是1....n个点。
邻接表
![](https://i-blog.csdnimg.cn/blog_migrate/009f7698624fcef2c382e79666dc4186.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2866e18e1ed3716c285d4c5499d4bdfb.png)
图遍历介绍
所谓图的遍历,即是对结点的访问。一个图有那么多个结点,如何遍历这些结点,需要特定策略,一般有两种访问策略: (1)深度优先遍历 (2)广度优先遍历
7.1 图的深度优先遍历
深度优先遍历基本思想
图的深度优先搜索(Depth First Search) 。
深度优先遍历算法步骤
看一个具体案例分析:
7.2 图的广度优先遍历
广度优先遍历基本思想
图的广度优先搜索(Broad First Search) 。
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
广度优先遍历算法步骤
6.1 若结点w尚未被访问,则访问结点w并标记为已访问。
6.2 结点w入队列
6.3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
代码实现:
public class Graph {
ArrayList<String> vertexList;
int[][] edges;
boolean[] isVisited;
public static void main(String[] args) {
Graph graph = new Graph(8);
String[] vertexes = {"1","2","3","4","5","6","7","8"};
for (int i = 0; i < vertexes.length; i++) {
graph.addVertex(vertexes[i]);
}
graph.createGraph(0,1,1);
graph.createGraph(0,2,1);
graph.createGraph(1,3,1);
graph.createGraph(1,4,1);
graph.createGraph(3,7,1);
graph.createGraph(4,7,1);
graph.createGraph(2,5,1);
graph.createGraph(2,6,1);
graph.createGraph(5,6,1);
graph.show();
System.out.println();
graph.dfs();
System.out.println();
System.out.println();
graph.bfs();
}
public Graph(int n){
vertexList = new ArrayList<>(n);
edges = new int[n][n];
}
public void dfs(boolean[] isVisited, int i){
System.out.print(vertexList.get(i) + "->");
isVisited[i] = true;
int w = getFirstNeighbor(i);
while (w != -1){
while (!isVisited[w]){
dfs(isVisited, w);
}
w = getNextNeighbor(i, w);
}
}
public void dfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]){
dfs(isVisited, i);
}
}
}
public void bfs(boolean[] isVisited, int i){
LinkedList queue = new LinkedList();
System.out.print(vertexList.get(i) + "->");
isVisited[i]= true;
int u;
int w;
queue.add(i);
while (!queue.isEmpty()){
u = (Integer)queue.removeFirst();
w = getFirstNeighbor(u);
while (w != -1){
if (!isVisited[w]){
System.out.print(vertexList.get(w) + "->");
isVisited[w] = true;
queue.add(w);
}
w = getNextNeighbor(u, w);
}
}
}
public void bfs(){
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
if (!isVisited[i]){
bfs(isVisited, i);
}
}
}
public void createGraph(int row, int col, int weight){
edges[row][col] = weight;
edges[col][row] = weight;
}
public void addVertex(String vertex){
vertexList.add(vertex);
}
public void show(){
for (int i = 0; i < edges.length; i++) {
System.out.println(Arrays.toString(edges[i]));
}
}
public int getFirstNeighbor(int row){
for (int i = 0; i < vertexList.size(); i++) {
if (edges[row][i] > 0) {
return i;
}
}
return -1;
}
public int getNextNeighbor(int row, int col){
for (int i = col + 1; i < vertexList.size(); i++) {
if (edges[row][i] > 0){
return i;
}
}
return -1;
}
}
二、算法
1. 递归
实际应用场景:迷宫问题(回溯)、递归(Recursion)
递归的概念:递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
递归调用机制:
(1)打印问题
(2)阶乘问题
递归可以解决的问题:
递归需要遵守的重要规则:
1.1 迷宫问题
代码实现:
public class Migong {
public static void main(String[] args) {
int[][] migong = new int[8][7];
for (int i = 0; i < migong.length; i++) {
migong[i][0] = 1;
migong[i][6] = 0;
}
for (int i = 0; i < migong[0].length; i++) {
migong[0][i] = 1;
migong[7][i] = 1;
}
migong[2][1] = 1;
migong[2][2] = 1;
System.out.println("原始迷宫为:");
for (int[] rows : migong) {
for (int cols : rows) {
System.out.print(cols + "\t");
}
System.out.println();
}
System.out.println("走过的路线为:");
isWay(1,1, migong);
for (int[] rows : migong) {
for (int cols : rows) {
System.out.print(cols + "\t");
}
System.out.println();
}
}
public static boolean isWay(int x, int y, int[][] arr){
if (arr[6][5] == 2){
return true;
}else {
if (arr[x][y] == 0){
arr[x][y] = 2;
if (isWay(x +1, y, arr)){
return true;
}else if (isWay(x, y + 1, arr)){
return true;
}else if (isWay(x - 1, y, arr)){
return true;
}else if (isWay(x, y - 1, arr)){
return true;
}else {
arr[x][y] = 3;
return false;
}
}else {
return false;
}
}
}
}
1.2 八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
八皇后问题算法思路分析
说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第i+1个皇后,放在第i+1行的第val+1列
代码实现:
public class Queen8 {
static int[] queen;
static int count;
static int totalCount;
public static void main(String[] args) {
queen = new int[8];
count = 0;
totalCount = 0;
Queen8 queen8 = new Queen8();
queen8.check(0);
System.out.println("共有" + count + "种方法");
System.out.println("共遍历了" + totalCount + "次");
}
public Queen8() {
}
public void check(int n){
if (n == queen.length){
print();
return;
}else {
for (int i = 0; i < queen.length; i++) {
queen[n] = i;
if (judge(n)){
check(n+1);
}
}
}
}
public boolean judge(int n){
totalCount++;
for (int i = 0; i < n; i++) {
if (queen[n] == queen[i] || Math.abs(n - i) == Math.abs(queen[n] - queen[i])){
return false;
}
}
return true;
}
public void print(){
count++;
System.out.println(Arrays.toString(queen));
}
}
2. 排序算法
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
排序的分类:
1) 内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序。
2) 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
3) 常见的排序算法分类:
算法的时间复杂度:
度量一个程序(算法)执行时间的两种方法
这种方法可行 , 但是有两个问题:一是要想对设计的算法的运行性能进行评测,需要实际运行该程序;二是所得时间的统计量依赖于计算机的硬件、软件等环境因素 , 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快 。
通过分析某个算法的 时间复杂度 来判断哪个算法更优 .
时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
举例:
忽略常数项
T(n)=2n+20 | T(n)=2*n | T(3n+10) | T(3n) | |
1 | 22 | 2 | 13 | 3 |
2 | 24 | 4 | 16 | 6 |
5 | 30 | 10 | 25 | 15 |
8 | 36 | 16 | 34 | 24 |
15 | 50 | 30 | 55 | 45 |
30 | 80 | 60 | 100 | 90 |
100 | 220 | 200 | 310 | 300 |
300 | 620 | 600 | 910 | 900 |
忽略低次项:
T(n)=2n^2+3n+10 | T(2n^2) | T(n^2+5n+20) | T(n^2) | |
1 | 15 | 2 | 26 | 1 |
2 | 24 | 8 | 34 | 4 |
5 | 75 | 50 | 70 | 25 |
8 | 162 | 128 | 124 | 64 |
15 | 505 | 450 | 320 | 225 |
30 | 1900 | 1800 | 1070 | 900 |
100 | 20310 | 20000 | 10520 | 10000 |
忽略系数:
T(3n^2+2n) | T(5n^2+7n) | T(n^3+5n) | T(6n^3+4n) | |
1 | 5 | 12 | 6 | 10 |
2 | 16 | 34 | 18 | 56 |
5 | 85 | 160 | 150 | 770 |
8 | 208 | 376 | 552 | 3104 |
15 | 705 | 1230 | 3450 | 20310 |
30 | 2760 | 4710 | 27150 | 162120 |
100 | 30200 | 50700 | 1000500 | 6000400 |
时间复杂度总结:
常见的时间复杂度
说明:
1)常数阶O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
2)对数阶O(logn) (2为底)
说明:在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = logn (2为底)也就是说当循环 logn (2为底) 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn) (2为底) 。 O(logn) (2为底) 的这个2 时间上是根据代码变化的,i = i * 3 ,则是 O(logn) (3为底) .
3) 线性阶O(n)
说明:这段代码,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用O(n)来表示它的时间复杂度
4)线性对数阶O(nlogN)
说明:线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)
5)平方阶O(n²)
说明:平方阶O(n²) 就更容易理解了,如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n)
6) 立方阶O(n³)、K次方阶O(n^k)
说明:参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似
平均时间复杂度和最坏时间复杂度
算法的空间复杂度:
基本介绍
2.1 冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)
代码实现:
public class BubbleSort {
public static void main(String[] args) {
int count = 0;
int[] alist = {12, -1, 4, 19, 23, 36};
int temp = 0;
boolean flag = false;
for (int i = 0; i < alist.length - 1; i++) {
for (int j = 0; j < alist.length - 1 - i; j++) {
if (alist[j] < alist[j+1]){
flag = true;
temp = alist[j];
alist[j] = alist[j+1];
alist[j+1] = temp;
}
count++;
}
if (!flag){
break;
}else {
flag = false;
}
}
System.out.println(count);
for (int i = 0; i < alist.length; i++) {
System.out.print(alist[i] + "\t");
}
}
}
2.2 选择排序
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
选择排序(select sorting)也是一种简单的排序方法。
它的基本思想是:
第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;
第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换;
第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换;
…;
第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换;
…;
第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换;
总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
public static void SelectionSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
int index = i;
int max = arr[i];
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] > max){
max = arr[j];
index = j;
}
}
if (index != i) {
arr[index] = arr[i];
arr[i] = max;
}
}
}
2.3 插入排序
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序(Insertion Sorting)的基本思想是:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
代码实现:
public static void Insert(int[] arr){
for (int i = 1; i < arr.length; i++) {
int insertValue = arr[i];
int insertIndex = i - 1;
while (insertIndex >= 0 && insertValue < arr[insertIndex]){
arr[insertIndex + 1] = arr[insertIndex];
insertIndex -= 1;
}
arr[insertIndex +1] = insertValue;
}
}
2.4 希尔排序
简单插入排序存在的问题
我们看简单的插入排序可能存在的问题.
数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小), 这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
结论: 当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
希尔排序法介绍
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
代码实现:
// 交换法
public static void changeShell(int[] arr){
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (arr[j] > arr[j + gap]){
int temp = arr[j];
arr[j] = arr[j+gap];
arr[j+gap] = temp;
}
}
}
}
}
// 插入法
public static void insertShell(int[] arr){
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
for (int i = gap; i < arr.length; i++) {
int index = i;
int insertValue = arr[i];
while (index - gap >= 0 && insertValue < arr[index - gap]){
arr[index] = arr[index - gap];
index -= gap;
}
arr[index] = insertValue;
}
}
}
2.5 快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
代码实现:
public static void quickSort(int[] arr, int left, int right){
int l = left;
int r = right;
int pivot = arr[(left + right) / 2];
while (l < r){
while (arr[l] < pivot){
l++;
}
while (arr[r] > pivot){
r--;
}
if (l >= r){
break;
}
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
if (arr[l] == pivot){
r--;
}
if (arr[r] == pivot){
l++;
}
}
if (l == r){
l++;
r--;
}
if (left < r){
quickSort(arr, left, r);
}
if (l < right){
quickSort(arr, l, right);
}
}
2.6 归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。
合并相邻有序子序列:
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤
归并排序:
public static 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, mid, right, temp);
}
}
public static void merge(int[] arr, int left, int mid, int right, int[] temp){
int l = left;
int r = mid + 1;
int t = 0;
while (l <= mid && r <= right){
if (arr[l] < arr[r]){
temp[t] = arr[l];
l++;
t++;
}else {
temp[t] = arr[r];
r++;
t++;
}
}
while (l <= mid){
temp[t] = arr[l];
t++;
l++;
}
while (r <= right){
temp[t] = arr[r];
t++;
r++;
}
t = 0;
int leftTemp = left;
while (leftTemp <= right){
arr[leftTemp] = temp[t];
leftTemp++;
t++;
}
}
2.7 基数排序(桶排序)
基数排序基本思想:
基数排序图文说明
将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序。
代码实现:
public static void radixSort(int[] arr){
int[][] tong = new int[10][arr.length];
int[] tongN = new int[10];
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]){
max = arr[i];
}
}
int len = (max + "").length();
for (int i = 0, n = 1; i < len; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
int count = arr[j] / n % 10;
tong[count][tongN[count]] = arr[j];
tongN[count]++;
}
int index = 0;
for (int j = 0; j < tongN.length; j++) {
if (tongN[j] != 0){
for (int k = 0; k < tongN[j]; k++) {
arr[index++] = tong[j][k];
}
}
tongN[j] = 0;
}
}
}
public static void radix2(int[] arr){
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]){
max = arr[i];
}
}
int len = (max + "").length();
int[][] tong = new int[10][arr.length];
int[] tongNum = new int[10];
for (int i = 0, n = 1; i < len; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
int m = arr[j] / n % 10;
tong[m][tongNum[m]] = arr[j];
tongNum[m]++;
}
int index = 0;
for (int j = 0; j < tongNum.length; j++) {
if (tongNum[j] != 0){
for (int k = 0; k < tongNum[j]; k++) {
arr[index++] = tong[j][k];
}
}
tongNum[j] = 0;
}
}
}
基数排序的说明:
常用排序算法总结和对比
相关术语解释:
3. 查找算法
时间复杂度:
线性查找:3 * n+ 3;
二分查找:(floor(logn) +1) * 5 + 4
3.1 顺序查找
判断数列中是否包含此名称【顺序查找】 要求: 如果找到了,就提示找到,并给出下标值。
3.2 二分查找
二分查找的时间复杂度:O(logn);空间复杂度:O(1),因为只需要常数个指针 i,j,m
代码实现:
public static int binarySearch(int[] arr, int left, int right, int value){
int mid = (left + right) / 2;
if (left <= right){
if (value < arr[mid]){
return binarySearch(arr, left, mid, value);
}else if (value > arr[mid]){
return binarySearch(arr, mid + 1, right, value);
}else {
return mid;
}
}else {
return -1;
}
}
public static List<Integer> binarySearch2(int[] arr, int left, int right, int value){
int mid = (left + right) / 2;
ArrayList<Integer> list = new ArrayList<>();
if (left > right){
return null;
}
if (value < arr[mid]){
return binarySearch2(arr, left, mid, value);
}else if (value > arr[mid]){
return binarySearch2(arr, mid + 1, right, value);
}else {
int temp = mid - 1;
while (temp >= 0 && arr[temp] == arr[mid]){
list.add(temp);
temp--;
}
list.add(mid);
temp = mid + 1;
while (temp < arr.length && arr[temp] == arr[mid]){
list.add(temp);
temp++;
}
return list;
}
}
3.3 插值查找
key 就是前面我们讲的 findVal
对应前面的代码公式: int mid = left + (right – left) * ( findVal – arr [left]) / ( arr [right] – arr [left])
4)举例说明插值查找算法 1-100 的数组
差值查找注意事项:
3.4 斐波那契查找
斐波那契(黄金分割法)查找基本介绍:
斐波那契(黄金分割法)原理:
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即mid=low+F(k-1)-1(F代表斐波那契数列),如下图所示
对F(k-1)-1的理解:
代码实现:
public class FibonacciSearch {
public static int maxsize = 20;
public static void main(String[] args) {
int[] arr = {0,1,2,3,4,5,6,8,10,11,89,1000,1234};
int index = fibonacciSearch(arr, 1000);
System.out.println(index);
}
public static int[] fib(){
int[] fib = new int[maxsize];
fib[0] = 1;
fib[1] = 1;
for (int i = 2; i < maxsize; i++) {
fib[i] = fib[i - 1] + fib[i - 2];
}
return fib;
}
public static int fibonacciSearch(int[] arr, int value){
int left = 0;
int right = arr.length - 1;
int k = 0; //斐波那契分割数值的下标
int mid = 0;
int f[] = fib();
while (right > f[k] - 1){
k++;
}
int[] temp = Arrays.copyOf(arr, f[k]);
for (int i = right + 1; i < temp.length; i++) {
temp[i] = arr[right];
}
while (left <= right){
mid = left + f[k - 1] - 1;
if (value < temp[mid]){
right = mid - 1;
k--;
}else if (value > temp[mid]){
left = mid + 1;
k -= 2;
}else {
if (mid <= right){
return mid;
}else {
return right;
}
}
}
return -1;
}
}
4. 十种常用算法
4.1 二分查找(非递归形式)
数组 {1,3, 8, 10, 11, 67, 100}, 编程实现二分查找, 要求使用非递归的方式完成.
public static int binarySearch(int[] arr, int value){
int left = 0;
int right = arr.length - 1;
while (left <= right){
int mid = (left + right) / 2;
if (value == arr[mid]){
return mid;
}else if (value < arr[mid]){
right = mid - 1;
}else {
left = mid + 1;
}
}
return -1;
}
4.2 分治算法
分治算法的基本步骤
分治法在每一层递归上都有三个步骤:
分治(Divide-and-Conquer(P))算法设计模式如下:
if |P|≤n0
then return(ADHOC(P))
//将P分解为较小的子问题 P1 ,P2 ,…,Pk
for i←1 to k
do yi ← Divide-and-Conquer(Pi) 递归解决Pi
T ← MERGE(y1,y2,…,yk) 合并子问题
return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,…,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,…,Pk的相应的解y1,y2,…,yk合并为P的解。
分治算法最佳实践-汉诺塔
汉诺塔的传说
汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
假如每秒钟一次,共需多长时间呢?移完这些金片需要5845.54亿年以上,太阳系的预期寿命据说也就是数百亿年。真的过了5845.54亿年,地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。
汉诺塔游戏的演示和思路分析:
代码实现:
public static void hanoiTower(int num, char a, char b, char c){
if (num == 1){
System.out.println("第" + num + "个飞盘:" + a + "->"+ c);
}else {
hanoiTower(num - 1, a, c, b);
System.out.println("第" + num + "个飞盘:" + a + "->"+ c);
hanoiTower(num - 1, b, a, c);
}
}
4.3 动态规划
动态规划算法介绍
应用场景-背包问题
背包问题:有一个背包,容量为4磅 , 现有如下物品
物品 | 重量 | 价格 |
吉他(G) | 1 | 1500 |
音响(S) | 4 | 3000 |
电脑(L) | 3 | 2000 |
1) 要求达到的目标为装入的背包的总价值最大,并且重量不超出
(1) v[ i ][0]=v[0][j]=0; // 表示 填入表 第一行和第一列是 0
(2) 当w[i]> j 时:v[i][j]=v[i-1][j] // 当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个单元格的装入策略
(3) 当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}
// 当 准备加入的新增的商品的容量小于等于当前背包的容量,
// 装入的方式:
v[i-1][j]: 就是上一个单元格的装入的最大值
v[i] : 表示当前商品的价值
v[i-1][j-w[i]] : 装入i-1商品,到剩余空间j-w[i]的最大值
当j>=w[i]时: v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :
代码实现:
public class Dynamic {
public static void main(String[] args) {
int[] w = {1,4,3};
int[] v = {1500,3000,2000};
int[][] table = new int[4][5];
int[][] path = new int[4][5];
for (int i = 0; i < table.length; i++) {
table[i][0] = 0;
}
for (int i = 0; i < table[0].length; i++) {
table[0][i] = 0;
}
for (int i = 0; i < table.length; i++) {
System.out.println(Arrays.toString(table[i]));
}
System.out.println("+++++++++++++++++++++++++");
for (int i = 1; i < table.length; i++) {
for (int j = 1; j < table[0].length; j++) {
if (w[i-1] > j){
table[i][j] = table[i - 1][j];
}else {
if (table[i - 1][j] < v[i- 1] + table[i - 1][j - w[i - 1]]){
table[i][j] = v[i- 1] + table[i - 1][j - w[i - 1]];
path[i][j] = 1;
}else {
table[i][j] = table[i - 1][j];
}
}
}
}
for (int i = 0; i < table.length; i++) {
System.out.println(Arrays.toString(table[i]));
}
System.out.println("=======================================");
for (int i = 0; i < table.length; i++) {
System.out.println(Arrays.toString(path[i]));
}
System.out.println("=======================================");
int i = path.length - 1;
int j = path[0].length - 1;
while (i > 0 && j > 0){
if (path[i][j] == 1){
System.out.println("第" + i +"个物品被装进背包");
System.out.println();
j -= w[i - 1];
}
i--;
}
}
}
4.4 KMP算法
应用场景-字符串匹配问题
字符串匹配问题::
KMP算法介绍
KMP算法最佳应用-字符串匹配问题
字符串匹配问题::
![](https://i-blog.csdnimg.cn/blog_migrate/5be88208ddc777a9b58807c4314dc5fe.png)
![](https://i-blog.csdnimg.cn/blog_migrate/cdbfaa23535e5a37d255dda4f9c02d9b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/fcc8cc6b8065becb2bff0d8a6fdffe0e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/a7737afc37dc46d9657cce7a84608bce.png)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cea6c756a176ea9b0a99b1737a8f85.png)
public class kmp {
public static void main(String[] args) {
String str2 = "ABCDABD";
int[] next = getNext(str2);
System.out.println(Arrays.toString(next));
String str1 = "ABCDAB ABCDABCDABDE"; //BBC ABCDAB
int search = search(str1, str2, next);
System.out.println(search);
}
public static int search(String str1, String str2, int[] next){
for (int i = 0, j = 0; i < str1.length(); i++) {
while (j > 0 && str1.charAt(i) != str2.charAt(j)){
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)){
j++;
}
if (j == str2.length()){
return i - j + 1;
}
}
return -1;
}
public static int[] getNext(String nextStr){
int[] next = new int[nextStr.length()];
next[0] = 0;
for (int i = 1, j = 0; i < nextStr.length(); i++) {
while (j > 0 && nextStr.charAt(i) != nextStr.charAt(j)){
j = next[j - 1];
}
if (nextStr.charAt(i) == nextStr.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}
4.5 贪心算法
贪心算法介绍
应用场景-集合覆盖问题
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
广播台 | 覆盖地区 |
K1 | "北京", "上海", "天津" |
K2 | "广州", "北京", "深圳" |
K3 | "成都", "上海", "杭州" |
K4 | "上海", "天津" |
K5 | "杭州", "大连" |
2ⁿ -1 个 , 假设每秒可以计算 10 个子集, 如图 :
广播台数量n | 子集总数2ⁿ | 需要的时间 |
5 | 32 | 3.2秒 |
10 | 1024 | 102.4秒 |
32 | 4294967296 | 13.6年 |
100 | 1.26*100³º | 4x10²³年 |
目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合:
代码实现:
public class GreedyArithm {
public static void main(String[] args) {
HashMap<String, HashSet<String>> broadcast = new HashMap<>();
HashSet<String> areas1 = new HashSet<>();
areas1.add("北京");
areas1.add("上海");
areas1.add("天津");
HashSet<String> areas2 = new HashSet<>();
areas2.add("广州");
areas2.add("北京");
areas2.add("深圳");
HashSet<String> areas3 = new HashSet<>();
areas3.add("成都");
areas3.add("上海");
areas3.add("杭州");
HashSet<String> areas4 = new HashSet<>();
areas4.add("上海");
areas4.add("天津");
HashSet<String> areas5 = new HashSet<>();
areas5.add("杭州");
areas5.add("大连");
broadcast.put("K1", areas1);
broadcast.put("K2", areas2);
broadcast.put("K3", areas3);
broadcast.put("K4", areas4);
broadcast.put("K5", areas5);
ArrayList<String> allAreas = new ArrayList<>();
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
String tempKey = null;
HashSet<String> tempArea = new HashSet<>();
ArrayList<String> selects = new ArrayList<>();
while (!allAreas.isEmpty()){
tempKey = null;
for (String key : broadcast.keySet()) {
tempArea.clear();
HashSet<String> area = broadcast.get(key);
tempArea.addAll(area);
tempArea.retainAll(allAreas);
if (tempArea.size() > 0 && (tempKey == null || tempArea.size() > broadcast.get(tempKey).size())){
tempKey = key;
}
}
if (tempKey != null) {
selects.add(tempKey);
allAreas.removeAll(broadcast.get(tempKey));
}
}
System.out.println(selects);
}
}
贪心算法注意事项和细节
Prime:到不了的点间距离为10000,包括自身到自身;
Kruskal:到自身为0,到不了的距离为INF = Integer.MAX_VALUE;
Dijkstra:到不了的距离为N = 65535,包括自身到自身;
Floyd:到自身为0,到非直达的点距离为N;
4.6 Prim算法(最小生成树问题)
应用场景-修路问题
看一个应用场景和问题:
1)有胜利乡有7个村庄(A, B, C, D, E, F, G) ,现在需要修路把7个村庄连通
思路: 将10条边,连接即可,但是总的里程数不是最小.
正确的思路,就是尽可能的选择少的路线,并且每条路线最小,保证总里程数最少.
最小生成树
修路问题本质就是就是最小生成树问题, 先介绍一下最小生成树(Minimum Cost Spanning Tree),简称MST。给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树
普里姆算法介绍
(5)提示: 单独看步骤很难理解,我们通过代码来讲解,比较好理解.
普里姆算法最佳实践(修路问题)代码实现:
public class Prim {
// char[] vertexes = {'A','B','C','D','E','F','G'};
// int[][] matrix = {{}}
public static void main(String[] args) {
char[] vertexes = {'A','B','C','D','E','F','G'};
int[][] matrix = {{10000,5,7,10000,10000,10000,2},
{5,10000,10000,9,10000,10000,3},
{7,10000,10000,10000,8,10000,10000},
{10000,9,10000,10000,10000,4,10000},
{10000,10000,8,10000,10000,5,4},
{10000,10000,10000,4,5,10000,6},
{2,3,10000,10000,4,6,10000}};
boolean[] isVisit = new boolean[vertexes.length];
isVisit[0] = true;
int h1 = 0;
int h2 = 0;
for (int i = 1; i < vertexes.length; i++) {
int maxWeight = 10000;
for (int j = 0; j < vertexes.length; j++) {
for (int k = 0; k < vertexes.length; k++) {
if (isVisit[j] == true && isVisit[k] == false && matrix[j][k] < maxWeight){
maxWeight = matrix[j][k];
h1 = j;
h2 = k;
}
}
}
isVisit[h2] = true;
System.out.println(vertexes[h1] + "->" + vertexes[h2] + " : " + matrix[h1][h2]);
}
}
}
4.7 Kruskal算法(公交站问题)
应用场景-公交站问题
看一个应用场景和问题:
克鲁斯卡尔算法介绍
![](https://i-blog.csdnimg.cn/blog_migrate/6a79bd4cb83d161e5f0b27befe015b00.png)
![](https://i-blog.csdnimg.cn/blog_migrate/e463085e23579e449c29442e6876405b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/3ef9ca4d650a49a849d925632b77af50.png)
![](https://i-blog.csdnimg.cn/blog_migrate/929d61a7f258348eca6af932cb4b0fbc.png)
应用场景-公交站问题代码实现:
public class KruskalDemo {
char[] data;
int[][] matrix;
int vertex;
// static int nums = 0;
public static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] data = {'A','B','C','D','E','F','G'};
int vertex = data.length;
int[][] matrix = {
{0, 12, INF,INF,INF,16,14},
{12,0,10,INF,INF,7,INF},
{INF,10,0,3,5,6,INF},
{INF,INF,3,0,4,INF,INF},
{INF,INF,5,4,0,2,8},
{16,7,6,INF,2,0,9},
{14,INF,INF,INF,8,9,0}
};
KruskalDemo kruskalDemo = new KruskalDemo(data, matrix, vertex);
kruskalDemo.krus();
// KGraph[] edge = kruskalDemo.getEdge();
// for (KGraph kGraph : edge) {
// System.out.println(kGraph);
// }
}
public KruskalDemo(char[] data, int[][] matrix, int vertex) {
this.data = data;
this.matrix = matrix;
this.vertex = vertex;
}
public void krus(){
KGraph[] edge = getEdge();
sortEdges(edge);
int index = 0;
int nums = getNums();
int[] ends = new int[nums];
KGraph[] rets = new KGraph[nums];
for (int i = 0; i < nums; i++) {
int p1 = getIndex(edge[i].start);
int p2 = getIndex(edge[i].end);
int m = getEnds(ends, p1);
int n = getEnds(ends, p2);
if (m != n){
rets[index++] = edge[i];
ends[m] = n;
}
}
for (int i = 0; i < index; i++) {
System.out.println(rets[i ]);
}
}
public int getIndex(char ch){
for (int i = 0; i < vertex; i++) {
if (data[i] == ch){
return i;
}
}
return -1;
}
public KGraph[] getEdge(){
int nums = getNums();
KGraph[] edges = new KGraph[nums];
int index = 0;
for (int i = 0; i < vertex; i++) {
for (int j = i+1; j < vertex; j++) {
if (matrix[i][j] != INF){
edges[index++] = new KGraph(data[i], data[j], matrix[i][j]);
}
}
}
return edges;
}
public int getNums(){
int nums = 0;
for (int i = 0; i < vertex; i++) {
for (int j = i+1; j < vertex; j++) {
if (matrix[i][j] != INF){
nums++;
}
}
}
return nums;
}
public int getEnds(int[] ends, int i){
while (ends[i] != 0){
i = ends[i];
}
return i;
}
public void sortEdges(KGraph[] edges){
int nums = getNums();
for (int i = 0; i < nums - 1; i++) {
for (int j = 0; j < nums - i - 1; j++) {
if (edges[j].weight > edges[j+1].weight){
KGraph temp = edges[j];
edges[j] = edges[j+1];
edges[j+1] = temp;
}
}
}
}
}
class KGraph{
char start;
char end;
int weight;
public KGraph(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "KGraph{" +
"start=" + start +
", end=" + end +
", weight=" + weight +
'}';
}
}
4.8 Dijkstra算法
应用场景-最短路径问题
看一个应用场景和问题:
迪杰斯特拉(Dijkstra)算法介绍
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
迪杰斯特拉(Dijkstra)算法过程
1)设置出发顶点为v,顶点集合V{v1,v2,vi...},v到V中各顶点的距离构成距离集合Dis,Dis{d1,d2,di...},Dis集合记录着v到图中各顶点的距离(到自身可以看作0,v到vi距离对应为di)
迪杰斯特拉(Dijkstra)算法最佳应用-最短路径代码实现:
public class Dijkstra {
public static void main(String[] args) {
char[] vertex = {'A','B','C','D','E','F','G'};
final int N = 65535;
int[][] matrix = {
{N,5,7,N,N,N,2},
{5,N,N,9,N,N,3},
{7,N,N,N,8,N,N},
{N,9,N,N,N,4,N},
{N,N,8,N,N,5,4},
{N,N,N,4,5,N,6},
{2,3,N,N,4,6,N}
};
DGraph dGraph = new DGraph(vertex, matrix);
dGraph.djs(2);
dGraph.showDijs();
}
}
class DGraph{
char[] vertex;
int[][] matrix;
VisitVertex vv;
public DGraph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
}
public void showDijs(){
vv.show();
}
public void update(int index){
for (int i = 0; i < vertex.length; i++) {
int len = vv.getDis(index) + matrix[index][i];
if (!vv.isVisit(i) && len < vv.getDis(i)){
vv.updateDis(i , len);
vv.updatePre(index, i);
}
}
}
public void djs(int index){
vv = new VisitVertex(vertex.length, index);
update(index);
for (int i = 1; i < vertex.length; i++) {
index = vv.updateArr();
update(index);
}
}
}
class VisitVertex{
int[] already_arr;
int[] pre_visit;
int[] dis;
public VisitVertex(int length, int index) {
this.already_arr = new int[length];
this.pre_visit = new int[length];
this.dis = new int[length];
Arrays.fill(dis, 65535);
already_arr[index] = 1;
dis[index] = 0;
}
public boolean isVisit(int index){
return already_arr[index] == 1;
}
public int getDis(int index){
return dis[index];
}
public void updatePre(int index, int i){
pre_visit[i] = index;
}
public void updateDis(int index, int len){
dis[index] = len;
}
public int updateArr(){
int min = 65535, index = 0;
for (int i = 0; i < already_arr.length; i++) {
if (already_arr[i] == 0 && dis[i] < min){
min = dis[i];
index = i;
}
}
already_arr[index] = 1;
return index;
}
public void show(){
System.out.println();
System.out.println(Arrays.toString(already_arr));
System.out.println();
System.out.println(Arrays.toString(pre_visit));
System.out.println();
System.out.println(Arrays.toString(dis));
int count = 0;
char[] vertex = {'A','B','C','D','E','F','G'};
for (int di : dis) {
if (di != 65535){
System.out.println(vertex[count] + " : " + di);
}else {
System.out.println("N");
}
count++;
}
}
}
4.9 Floyd算法
弗洛伊德(Floyd)算法介绍
弗洛伊德(Floyd)算法图解分析
弗洛伊德(Floyd)算法最佳应用-最短路径
代码实现:
public class Floyd {
public static void main(String[] args) {
char[] vertex = {'A','B','C','D','E','F','G'};
final int N = 65535;
int[][] matrix = {
{0,5,7,N,N,N,2},
{5,0,N,9,N,N,3},
{7,N,0,N,8,N,N},
{N,9,N,0,N,4,N},
{N,N,8,N,0,5,4},
{N,N,N,4,5,0,6},
{2,3,N,N,4,6,0}
};
FGraph fGraph = new FGraph(vertex, matrix);
fGraph.floyd();
fGraph.show();
}
}
class FGraph{
char[] vertex;
int[][] matrix;
int[][] pre;
public FGraph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
this.pre = new int[vertex.length][vertex.length];
for (int i = 0; i < vertex.length; i++) {
Arrays.fill(pre[i], i);
}
}
public void show(){
System.out.println("前驱矩阵为:");
for (int[] pres : pre) {
System.out.println(Arrays.toString(pres));
}
System.out.println("路径图为:");
for (int[] dis : matrix) {
System.out.println(Arrays.toString(dis));
}
System.out.println();
char[] vertex = {'A','B','C','D','E','F','G'};
for (int i = 0; i < vertex.length; i++) {
for (int j = i + 1; j < vertex.length; j++) {
System.out.println(vertex[i] + " -> " + vertex[j] + " : " + matrix[i][j]);
}
}
}
public void floyd(){
int len = 0;
for (int k = 0; k < vertex.length; k++) {
for (int i = 0; i < vertex.length; i++) {
for (int j = 0; j < vertex.length; j++) {
len = matrix[i][k] + matrix[k][j];
if (len < matrix[i][j]){
matrix[i][j] = len;
pre[i][j] = pre[k][j];
}
}
}
}
}
}
4.10 马踏棋盘算法(骑士周游问题/dfs的应用)
马踏棋盘算法介绍和游戏演示
马踏棋盘游戏代码实现
public class HorseChessboard {
// static int[][] chessboard;
static boolean[] isVisit;
static boolean finished;
static int X;
static int Y;
public static void main(String[] args) {
X = 8;
Y = 8;
// int row = 1;
// int col = 1;
int[][] chessboard = new int[X][Y];
isVisit = new boolean[X * Y];
System.out.println("算法开始运行……");
long start = System.currentTimeMillis();
visitChessBoard(chessboard, 0, 0, 1);
long end = System.currentTimeMillis();
System.out.println("共需要" + (end - start) + "ms");
// for (int[] rows : chessboard) {
// for (int col : rows) {
// System.out.print(col + "\t");
// }
// System.out.println();
// }
for (int i = 0; i < chessboard.length; i++) {
for (int j = 0; j < chessboard[0].length; j++) {
System.out.print(chessboard[i][j] + "\t");
}
System.out.println();
}
}
public static void visitChessBoard(int[][] chessboard, int row, int col, int step) {
chessboard[row][col] = step;
System.out.println(row + ", " + col + " : " + step);
isVisit[row * X + col] = true;
List<Point> next = next(new Point(row, col));
sort(next);
while (!next.isEmpty()) {
Point p = next.remove(0);
if (!isVisit[p.x * X + p.y]) {
visitChessBoard(chessboard, p.x, p.y, step + 1);
}
}
if (step < X * Y && !finished) {
chessboard[row][col] = 0;
isVisit[row * X + col] = false;
} else {
finished = true;
}
}
public static List<Point> next(Point curPoints) {
ArrayList<Point> ps = new ArrayList<>();
Point p1 = new Point();
if ((p1.x = curPoints.x + 2) < X && (p1.y = curPoints.y - 1) >= 0) { //0 //
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x + 2) < X && (p1.y = curPoints.y + 1) < Y) { //1
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x + 1) < X && (p1.y = curPoints.y + 2) < Y) { //2 /
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x - 1) >= 0 && (p1.y = curPoints.y + 2) < Y) { //3
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x - 2) >= 0 && (p1.y = curPoints.y + 1) < Y) { //4 ///
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x - 2) >= 0 && (p1.y = curPoints.y - 1) >= 0) { //5
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x - 1) >= 0 && (p1.y = curPoints.y - 2) >= 0) { //6 ///
ps.add(new Point(p1));
}
if ((p1.x = curPoints.x + 1) < X && (p1.y = curPoints.y - 2) >= 0) { //7 ///
ps.add(new Point(p1));
}
return ps;
}
public static void sort(List<Point> points) {
points.sort(new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
int count1 = next(o1).size();
int count2 = next(o2).size();
if (count1 < count2) {
return -1;
} else if (count1 == count2) {
return 0;
} else {
return -1;
}
}
});
}
}