
本文共 44506 字,大约阅读时间需要 148 分钟。
目录
bilibili尚硅谷韩顺平老师课程的笔记。
数据结构和算法简介
数据结构与算法的重要性:
- 算法是程序的灵魂,优秀的程序可以在海量数据计算时,依然保持高速计算;
- 一般来讲,程序会使用了内存计算框架(比如Spark)和缓存技术(比如Redis等)来优化程序,再深入的思考一下,这些计算框架和缓存技术, 它的核心功能是哪个部分呢?
- 拿实际工作经历来说, 在Unix下开发服务器程序,功能是要支持上千万人同时在线, 在上线前,做内测,一切OK,可上线后,服务器就支撑不住了, 公司的CTO对代码进行优化,再次上线,坚如磐石。你就能感受到程序是有灵魂的,就是算法;
- 目前程序员面试的门槛越来越高,很多一线IT公司(大厂),都会有数据结构和算法面试题(负责的告诉你,肯定有的);
- 如果你不想永远都是代码工人,那就花时间来研究下数据结构和算法。
数据结构和算法的关系:
- 数据data结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构.学好数据结构可以编写出更加漂亮,更加有效率的代码;
- 要学习好数据结构就要多多考虑如何将生活中遇到的问题,用程序去实现解决;
- 程序 = 数据结构 + 算法;
- 数据结构是算法的基础, 换言之,想要学好算法,需要把数据结构学到位。
数据结构包括:(线性结构、非线性结构)
线性结构:
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系;
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构;
- 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的;
- 链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息;
- 线性结构常见的有:数组、队列、链表和栈;
非线性结构:非线性结构包括:二维数组,多维数组,广义表,树结构,图结构。
稀疏数组
应用:使用稀疏数组,来保留类似前面的二维数组(棋盘、地图等等) 把稀疏数组存盘,并且可以从新恢复原来的二维数组数。
代码 :
package com.jiao.dataStructure;import java.io.*;import java.util.ArrayList;import java.util.List;public class SparseArr { public static void main(String[] args) { //创建二维数组rootArr[11][11]:0-无棋子,1-黑子,2-白子 int rootArr[][] = new int[11][11]; rootArr[1][2] = 1; rootArr[2][3] = 2; //遍历rootArr System.out.println("===================原始数组================"); for (int[] row1 : rootArr) { for (int data : row1) { System.out.printf("%d\t",data); } System.out.println(); } /** 二维数组 转 稀疏数组 的思路 1. 遍历 原始的二维数组,得到有效数据的个数sum; 2. 根据sum的值就可以创建 稀疏数组sparseArr int[sum+1][3]; 3. 将二维数组的有效数据存入到 稀疏数组。 */ int sum=0; //原始数组有效数据个数统计 for (int[] row2 : rootArr) { for (int data : row2) { if (data != 0){ sum ++; } } } System.out.println("原始数组有效数据个数:"+sum); int sparseArr[][] = new int[sum+1][3]; //创建稀疏数组 //稀疏数组第一行固定:[0][0]、[0][1]原数组行列数,[0][2]原数组有效数值个数 sparseArr[0][0]=11; sparseArr[0][1]=11; sparseArr[0][2]=sum; //遍历原数组,将有效数值赋值给稀疏数组 int row = 1; //稀疏数组从第二行开始,每一行进行sum次写入 for (int i = 0; i < 11; i++) { for (int j = 0; j < 11; j++) { if (rootArr[i][j] != 0){ //原始数组=1,写入稀疏数组 sparseArr[row][0] = i; sparseArr[row][1] = j; sparseArr[row][2] = rootArr[i][j]; row ++; } } } //输出稀疏数组 System.out.println("==============原始数组 转 稀疏数组==========="); for (int[] row3 : sparseArr) { for (int data : row3) { System.out.printf("%d\t",data); } System.out.println(); } //作业1:将稀疏数组写进文件 //作业2:将文件的稀疏数组读出来/* System.out.println("=================读取文件的稀疏数组================="); for (int[] row6 : sparseArr1) { for (int data : row6) { System.out.printf("%d\t",data); } System.out.println(); }*/ /** 稀疏数组 转 原始的二维数组 的思路 1. 先读取稀疏数组的第一行,根据第一行的数据‘’‘’‘’‘’,创建原始的二维数组,比如图片中的 chessArr2=int[11][11]; 2. 再读取稀疏数组后几行的数据,并赋给 原始二维数组 即可。 */ System.out.println("==============稀疏数组 转 原始数组==========="); int rowChange = sparseArr1[0][0]; int colChange = sparseArr1[0][1]; System.out.println("原数组row:"+rowChange+"\t原数组col:"+colChange); int changeArr[][] = new int[rowChange][colChange]; //根据读取的稀疏数组第一行创建要恢复的二维数组 for (int i = 1; i <= sparseArr[0][2]; i++) { changeArr[sparseArr1[i][0]][sparseArr1[i][1]] = sparseArr1[i][2]; //把稀疏数组每行row、col、val 读出来做出相应的赋值 } //输出恢复的二维数组 for (int[] row4 : changeArr) { for (int data : row4) { System.out.printf("%d\t",data); } System.out.println(); } }}
作业:
- 在前面的基础上,将稀疏数组保存到磁盘上,比如 map.data;
- 恢复原来的数组时,读取map.data 进行恢复。
//作业1:将稀疏数组写进文件 //1.创建字符输出流 FileWriter writer = null; try { //2.数据写入的路径 File file = new File("map.data"); //3.文件不存在就创建 if (!file.exists()){ file.createNewFile(); } //4.给字符输出流赋予实例 writer = new FileWriter(file); //5.稀疏数组循环写入文档 for (int i = 0; i < sparseArr.length; i++) { //6.以|分隔每行前n-1个数据 for (int j = 0; j < sparseArr[0].length-1; j++) { writer.write(sparseArr[i][j]+"|"); } //7.数组最后一行不加| writer.write(sparseArr[i][sparseArr[0].length-1]+""); //8.加上换行符 writer.write("\n"); } //9.把writer数据刷新一次,全部写入文件中 writer.flush(); } catch (IOException e) { e.printStackTrace(); } finally { //10.假如writer不为空,就将其关闭 if (writer != null){ try { writer.close(); System.out.println("稀疏数组写入完成..."); } catch (IOException e) { e.printStackTrace(); System.out.println("稀疏数组写入失败..."); } } }//作业2:将文件的稀疏数组读出来 //1.申明一个字符输入流 FileReader reader = null; //2.申明一个字符输入缓冲流 BufferedReader readerBuf = null; //3.申明一个从文件中读取的二维数组(稀疏数组changeArr1) int sparseArr1[][] = null; try { //4.指定reader的读取路径 reader = new FileReader("map.data"); //5.通过BufferReader包装字符流 readerBuf = new BufferedReader(reader); //6.创建一个List集合,用来存放读取的文件数据 ListstrList = new ArrayList<>(); //7.用来存放一行的数据 String lineStr; //8.逐行读取文件的内容 while ((lineStr = readerBuf.readLine()) != null){ //9.把读取的行添加到List中 strList.add(lineStr); } //10.获取文件的行数 int lineNum = strList.size(); //11.获取数组的列数 String s = strList.get(0); int columnNum = s.split("\\|").length; //12.根据行列创建数组 sparseArr1 = new int[lineNum][columnNum]; //13.记录输出当前行 int count = 0; //14.循环遍历集合,把集合中的数据放入数组中 for (String str : strList) { //15.将读取的str按照“|”分割,用字符串数组接收 String[] strs = str.split("\\|"); for (int i = 0; i < columnNum; i++) { sparseArr1[count][i] = Integer.valueOf(strs[i]); } //16.将行数+1 count ++; } } catch (Exception e) { e.printStackTrace(); } finally { //17.关闭字符输入流 try { reader.close(); } catch (IOException e) { e.printStackTrace(); } //18.关闭字符输入缓冲流 try { readerBuf.close(); } catch (IOException e) { e.printStackTrace(); } }
复习:数组、列表、文件操作
队列
队列介绍:
- 队列是一个有序列表,可以用数组或是链表来实现;
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出;
- 示意图:(使用数组模拟队列示意图):
数组模拟队列:
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如上图, 其中 maxSize 是该队列的最大容量;
- 因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变。
- 当我们将数据存入队列时称为”addQueue”,addQueue 的处理需要有两个步骤:思路分析
- 将尾指针往后移:rear+1 , 当front == rear 【空】
- 若尾指针 rear 小于队列的最大下标 maxSize-1,则将数据存入 rear所指的数组元素中,否则无法存入数据。 rear == maxSize - 1[队列满]
1. 写一个类:Class arrayQueue{ - //变量、构造器 - //创建队列的构造器 - //判断队列是否为空 boolean isEmpty(); - //判断队列是否满 boolean isFull(); - //添加数据到队列(判断满?rear++) void addQueue(int n); - //获取队列的数据,出队列(判断空?抛异常,front++) int getQueue(); - //显示队列的所有数据(遍历,判断空?) void showQueue(); - //显示队列的头数据(不是取数据,判断空?) int headQueue();}2. 测试: switch语句: - show:显示队列、 - exit:退出队列、 - add:添加数据到队列、 - get:从队列中取数据、 - head:查看队列头的数据3.问题:一次性数组。
代码:
package com.jiao.dataStructure;import java.util.Scanner;public class ArrayQueue { public static void main(String[] args) { //创建一个队列 arrQueue queue = new arrQueue(3); //接收用户的输入菜单 char key = ' '; Scanner scanner = new Scanner(System.in); boolean loop = true; //菜单循环 while (loop){ //输出一个菜单 System.out.println("----------------"); System.out.println("s(show):显示队列"); System.out.println("e(exit):退出队列"); System.out.println("a(add):添加数据到队列"); System.out.println("g(get):从队列中取数据"); System.out.println("h(head):查看队列头的数据"); key = scanner.next().charAt(0); switch (key){ case 's': queue.showQueue(); break; case 'a': System.out.printf("添加一条数:"); int data = scanner.nextInt(); queue.addQueue(data); break; case 'g': try { int result = queue.getQueue(); System.out.printf("队列获取的数据是:%d",result); } catch (Exception e){ System.out.println(e.getMessage()); } break; case 'h': try { int headQueue = queue.headQueue(); System.out.printf("队列获取的头数据是:%d",headQueue); } catch (Exception e){ System.out.println(e.getMessage()); } break; case 'e': scanner.close(); loop=false; break; default: break; } } System.out.println("程序已经退出。"); }}//数组模拟队列类class arrQueue{ //数组模拟队列所需的变量 private int maxSize; //数组队列的容量 private int front; //数组队列的最前元素 private int rear; //数组队列的最后元素 private int[] arr; //数组队列数据 //构造器 public arrQueue(int size){ maxSize = size; arr = new int[size]; //创建 包含容量的队列数据 front = -1; //指向队列的前一个位置,不包含此位置 rear = -1; //指向队列的最后一个位置,包含此位置 } //判断队列是否满 public boolean isFull(){ return rear == maxSize - 1; } //判断队列是否为空 public boolean isEmpty(){ return front == rear; } //添加数据到队列 public void addQueue(int n){ if (isFull()){ System.out.println("队列满了,不能加数据。"); return; } rear++; arr[rear]=n; } //获取队列的数据,出队列 public int getQueue(){ if (isEmpty()) throw new RuntimeException("队列为空,没有数据~"); front++; return arr[front]; } //显示队列所有的数据 public void showQueue(){ if (isEmpty()){ System.out.println("队列为空,没有数据~"); return; } for (int i = 0; i < arr.length; i++) { System.out.printf("arr[%d]=%d, ",i,arr[i]); } System.out.println(); } //显示队列的头数据 public int headQueue(){ if (isEmpty()){ System.out.println("队列为空,没有数据~"); return -1; } return front+1; }}
结果测试:
问题:队列只能使用一次。
数组模拟环形队列:
思路:
- front指向队列的第一个元素:初始值front = 0;
- real指向队列最后一个元素的后一个位置(预留空间):初始值real = 0;
- 队列满:(rear+1)%maxSize==front;
- 队列空:rear==front;
- 有效数据个数:(rear+maxSize-front)%maxSize。
代码:
package com.jiao.dataStructure;import java.util.Scanner;public class CircleArrayQueue { public static void main(String[] args) { //创建一个队列 circleQueue queue = new circleQueue(4); //接收用户的输入菜单 char key = ' '; Scanner scanner = new Scanner(System.in); boolean loop = true; //菜单循环 while (loop){ //输出一个菜单 System.out.println("----------------"); System.out.println("s(show):显示队列"); System.out.println("e(exit):退出队列"); System.out.println("a(add):添加数据到队列"); System.out.println("g(get):从队列中取数据"); System.out.println("h(head):查看队列头的数据"); key = scanner.next().charAt(0); switch (key){ case 's': queue.showQueue(); break; case 'a': System.out.printf("添加一条数:"); int data = scanner.nextInt(); queue.addQueue(data); break; case 'g': try { int result = queue.getQueue(); System.out.printf("队列获取的数据是:%d",result); } catch (Exception e){ System.out.println(e.getMessage()); } break; case 'h': try { int headQueue = queue.headQueue(); System.out.printf("队列获取的头数据是:%d",headQueue); } catch (Exception e){ System.out.println(e.getMessage()); } break; case 'e': scanner.close(); loop=false; break; default: break; } } System.out.println("程序已经退出。"); }}//数组模拟环形队列class circleQueue { //数组模拟队列所需的变量 private int maxSize; //数组队列的容量 private int front; //数组队列的最前元素 private int rear; //数组队列的最后元素 private int[] arr; //数组队列数据 //构造器 public circleQueue(int size) { maxSize = size; arr = new int[size]; //创建 包含容量的队列数据 front = 0; //指向队列的第一个位置 rear = 0; //指向队列的最后一个位置 } //判断队列是否满 public boolean isFull() { return (rear + 1) % maxSize == front; } //判断队列是否为空 public boolean isEmpty() { return front == rear; } //添加数据到队列 public void addQueue(int n) { if (isFull()) { System.out.println("队列满了,不能加数据。"); return; } arr[rear] = n; //real初始为0,先使用再指向预留字段 rear = (rear + 1) % maxSize; //不能单纯的rear++ } //获取队列的数据,出队列 public int getQueue() { if (isEmpty()) throw new RuntimeException("队列为空,没有数据~"); //front指向第一个元素 //1.front保存临时变量(不保存后移就找不到了) //2.front后移 //3.返回临时变量 int value = arr[front]; front = (front+1)%maxSize; return value; } //显示队列所有的数据 public void showQueue(){ if (isEmpty()){ System.out.println("队列为空,没有数据~"); return; } //从front开始遍历,遍历多少个数据 for (int i = front; i < front+size(); i++) { System.out.printf("arr[%d]=%d\n",i%maxSize,arr[i%maxSize]); } System.out.println(); } //有效数据个数(n-1),第n个是预留空间 //获取当前循环队列数据个数 public int size(){ return (rear+maxSize-front)%maxSize; } //显示队列的头数据 public int headQueue(){ if (isEmpty()){ System.out.println("队列为空,没有数据~"); return -1; } return front; }}
测试结果:
复习:类、对象、构造器等
链表
单向链表:
案例:
- 使用带head头的单向链表实现 –水浒英雄排行榜管理 完成对英雄人物的增删改查操作,
- 第一种方法在添加英雄时,直接添加到链表的尾部;
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置 (如果有这个排名,则添加失败,并给出提示) 。
1. 创建(添加)和遍历
class HeroNode{ int no; String name; String nickname; HeroNode next;}
添加:
- 先创建一个head头结点,作用就是表示单链表的头
- 后面每添加一个节点,就直接加入到 链表的最后
遍历:
- 通过一个辅助变量遍历,帮助遍历整个链表
代码:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入 heroes.add(heroNode1); heroes.add(heroNode2); heroes.add(heroNode3); heroes.add(heroNode4); //显示 heroes.list(); }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); //1.添加节点到单向链表 //思路(不考虑编号顺序): 1.找到当前链表的最后节点; 2.将最后这个节点next,指向 新的节点 public void add(HeroNode heroNode) { //head节点不能动,用辅助遍历temp HeroNode temp = head; //临时存放头节点 while (true) { if (temp.next == null) { //找到链表的最后 break; } temp = temp.next; //没找到temp后移 } temp.next = heroNode; //while退出循环,temp指向最后,找到了 } //2.显示遍历 public void list() { //判断链表是否为空 if (head.next == null) { System.out.println("链表为空~~~"); return; } //辅助temp来遍历 HeroNode temp = head.next; while (true) { if (temp == null) { //判断链表到最后 break; } System.out.println(temp); //打印 temp = temp.next; //指向下一个节点 } }}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode { //变量 public int no; //英雄编号 public String name; //英雄名字 public String nickname; //英雄昵称 public HeroNode next; //下一个节点地址 //构造器 public HeroNode(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //为了显示方便,重新toString @Override public String toString() { return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickname='" + nickname + '\'' + '}'; }}
测试:
2. 添加(按照编号顺序)
- a. 首先找到新添加的节点的位置,是通过辅助变量(指针),遍历;
- b. 新的节点.next=temp.next;
- c. 将temp.next=新的节点。
代码:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入 heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); heroes.addByOrder(heroNode2); //显示 heroes.list(); }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"","");... //添加的第二种方法 public void addByOrder(HeroNode heroNode) { //1. 找到No的前一个位置,temp变量,遍历 HeroNode temp = head; //临时变量存放头指针 boolean flag = false; //是否重复的标志 while (true) { if (temp.next == null) { //找到了最后 break; } if (temp.next.no > heroNode.no) { //temp.next的no开始大于要插入的no,说明当前的temp找到了 break; } else if ( temp.next.no == heroNode.no) { //要添加的no在链表存在了 flag = true; break; } temp = temp.next; } //2.通过flag,判定添加 if (flag) { System.out.printf("准备插入的英雄编号:%d已经存在,不能添加!\n",heroNode.no); } else { heroNode.next = temp.next; //temp.next的指向 放在 HeroNode.next的指向 temp.next = heroNode; //把要插入的节点 放在 temp.next } }... }
测试结果:
3.节点的修改(根据编号修改,不修改编号)
代码段:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //修改 System.out.println("修改前:"); heroes.list(); //修改前 HeroNode newHero = new HeroNode(2,"鲁智深1","花和尚1"); heroes.update(newHero); //显示 System.out.println("修改后:"); heroes.list(); //修改后 }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"","");... //3.修改(根据新的数据的no修改) public void update(HeroNode newHeroNode) { if (head.next == null) { //判断是否为空 System.out.println("链表为空~"); return; } HeroNode temp = head.next; //辅助变量 boolean flag = false; //标志是否找到 while (true) { if (temp.next == null) { //到了链表最后,没有找到同 no break; } if (temp.no == newHeroNode.no) { //找到no相同的temp flag = true; break; } temp = temp.next; } if (flag) { //找到了 temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; } else { //没找到 System.out.printf("没有找到要修改的no=%d的节点。",newHeroNode.no); } }}...
测试结果:
4.删除节点
- a. 先找到 需要删除的 这个节点的前一个节点temp
- b. temp.next = temp.next.next
- c. 被删除的节点,将不会有其他引用指向,会被垃圾回收
代码段:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //heroes.addByOrder(heroNode2); //删除 heroes.del(2); heroes.del(2); //heroes.del(4); //heroes.del(3); //显示 //System.out.println("修改后:"); heroes.list(); }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"","");... //4.删除(根据no) public void del(int no){ //找到需要删除的节点的前一个:temp.next.no = no if (head.next == null) { //判断是否为空 System.out.println("链表为空~"); return; } HeroNode temp = head; //辅助变量 boolean flag = false; //标志是否找到 while (true){ if (temp.next ==null){ //到链表最后也没找到 break; } if (temp.next.no == no){ //找到了,temp是找到的前一个节点 flag = true; break; } temp = temp.next; } if (flag){ temp.next = temp.next.next; //前一个的next地址 = 要删除的节点的next(指向的是要删除节点的下一个节点) } else { System.out.printf("没有找到要删除的节点no=%d。\n",no); } }}...
测试结果:
面试题:
- 1. 求单链表中有效节点的个数(不统计头结点)
代码段:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //heroes.addByOrder(heroNode2); //删除 heroes.del(2); //显示 heroes.list(); //修改后 //测试:获取单链表个数 System.out.println("有效节点个数:"+getLength(heroes.getHead())); } //1. 求单链表中有效节点的个数(不统计头结点) public static int getLength(HeroNode head){ if (head.next == null) { //空链表 return 0; } HeroNode cur = head.next; //临时变量 int length = 0; //统计个数 while (cur != null) { //遍历 length++; cur = cur.next; //指针后移 } return length; }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); public HeroNode getHead() { return head; } ...}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode {...}
测试结果:
- 2. 查找单链表中的倒数第k个结点 【新浪面试题】
步骤:
- 编写一个方法,接收head节点,同时接收一个index(表示倒数第index个节点)
- 把链表从头到尾遍历,得到链表的总长度 (调用getLength()方法)
- 得到size后,从链表的第一个开始遍历(size-index)个,就可以得到
代码段:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //heroes.addByOrder(heroNode2); //删除 heroes.del(2); //显示 //System.out.println("修改后:"); heroes.list(); //修改后 //测试:输出倒数第k个节点 int index = 1; HeroNode heroNo = listOneHeroNode(heroes.getHead(), index); System.out.println("倒数第"+index+"节点是:"+heroNo); } //2. 查找单链表中的倒数第k个结点 【新浪面试题】 /* 分析: 1) 编写一个方法,接收head节点,同时接收一个index(表示倒数第index个节点) 2) 把链表从头到尾遍历,得到链表的总长度 (调用getLength()方法) 3) 得到size后,从链表的第一个开始遍历(size-index)个,就可以得到 */ public static HeroNode listOneHeroNode(HeroNode head, int index){ if (head.next == null) { System.out.println("链表为空~"); return null; } int size = getLength(head); //链表有效个数 HeroNode temp = head.next; //temp指向head后一个 int sum = 0;//统计正查询个数 if (size < index || index <=0 ){ System.out.println("查询的倒数第"+index+"节点不存在~"); return null; //返回null } else { while (temp != null) { //遍历 if (sum == (size - index)) { break; } temp = temp.next; //后移 sum++; } } //老师的方法 /*for (int i = 0; i < size - index; i++) { temp = temp.next; }*/ return temp; } ...}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); public HeroNode getHead() { return head; } ...}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode {...}
测试结果:
- 3. 单链表的反转【腾讯面试题,有点难度】
步骤:
- a) 先定义一个节点reverseHead = new HeroNode();
- b) 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前面;
- c) 原来的链表的head.next = reverseHead.next;
代码:
package com.jiao.dataStructure;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //显示 System.out.println("修改前:"); heroes.list(); reverseList(heroes.getHead()); System.out.println("修改后:"); heroes.list(); } /* 3. 单链表的反转【腾讯面试题,有点难度】 步骤: a) 先定义一个节点reverseHead = new HeroNode(); b) 从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前面; c) 原来的链表的head.next = reverseHead.next; */ public static void reverseList(HeroNode head){ //如果当前的链表为空,或者只有一个节点,无需反转,直接返回 if (head.next == null || head.next.next == null) { return; } //定义一个辅助的指针(变量)cur,帮助我们遍历原来的链表,next:cur的下一个节点 HeroNode cur = head.next; HeroNode next = null; //a.新链表reverseHead HeroNode reverseHead = new HeroNode(0,"",""); //b.遍历 while (cur != null) { next = cur.next; //临时保存要取走的节点的下一个节点 cur.next = reverseHead.next; //取后 先连接reverseHead的原来的头结点的下一个,再把新的放在最前面 reverseHead.next = cur; //取出的节点放在reverseHead的最前面 cur = next; //当前节点的下一个 } //c.把reverseHead头换成head的头节点 head.next = reverseHead.next; }}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); public HeroNode getHead() { return head; }...}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode {...}
测试结果:
- 4. 从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
代码:
package com.jiao.dataStructure;import java.util.Stack;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); //创建链表 HeroManagement heroes = new HeroManagement(); //加入(按照编号排序) heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode2); //显示 System.out.println("修改前:"); heroes.list(); System.out.println("用栈反转打印链表数据:"); reversePrint(heroes.getHead()); System.out.println("修改后:"); heroes.list(); } //4.利用栈这个数据结构,将各个节点压到栈中,利用栈的先进后出特点,实现逆序打印 public static void reversePrint(HeroNode head){ //判断空 if (head.next == null) { return; } //创建栈stack,将各个节点压入栈中 push Stackstack = new Stack<>(); HeroNode cur = head.next; //临时变量 while (cur != null) { stack.push(cur); //链表节点入栈 cur = cur.next; } //将栈中的节点打印,pop出栈 while (stack.size() > 0) { System.out.println(stack.pop()); } }...}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); public HeroNode getHead() { return head; }...}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode {...}
测试结果:
- 5. 合并两个有序的单链表,合并之后的链表依然有序【课后练习作业】
代码:
package com.jiao.dataStructure;import java.util.Stack;public class SingleLinkedList { public static void main(String[] args) { //test //先创建节点数据 HeroNode heroNode1 = new HeroNode(1,"松江","及时雨"); HeroNode heroNode2 = new HeroNode(2,"鲁智深","花和尚"); HeroNode heroNode3 = new HeroNode(3,"李逵","黑旋风"); HeroNode heroNode4 = new HeroNode(4,"林冲","豹子头"); HeroNode heroNode5 = new HeroNode(5,"公孙胜","入云龙"); HeroNode heroNode6 = new HeroNode(6,"花荣","小李广"); HeroNode heroNode7 = new HeroNode(7,"柴进","小旋风"); HeroNode heroNode8 = new HeroNode(8,"张顺 ","浪里白条"); //创建链表 HeroManagement heroes1 = new HeroManagement(); HeroManagement heroes2 = new HeroManagement(); //加入(按照编号排序) heroes1.addByOrder(heroNode1); heroes1.addByOrder(heroNode4); heroes1.addByOrder(heroNode8); heroes1.addByOrder(heroNode5); //heroes2.addByOrder(heroNode3); heroes2.addByOrder(heroNode7); heroes2.addByOrder(heroNode6); heroes2.addByOrder(heroNode2); //显示 System.out.println("修改前:"); heroes1.list(); System.out.println("-----"); heroes2.list(); System.out.println("两个链表合并:"); HeroNode combination = listDoubleCombination(heroes1.getHead(), heroes2.getHead()); HeroNode temp = combination.next; while (temp != null) { System.out.println(temp.toString()); temp = temp.next; } } /* 5. 合并两个有序的单链表,合并之后的链表依然有序【no排序】 思路: 依次比较两个有序的链表,再放入新的链表 */ public static HeroNode listDoubleCombination(HeroNode head1, HeroNode head2) { //判断为空 if (head1.next == null && head2.next == null) { return null; } if (head1.next == null && head2.next != null) { return head2; //head1无节点,head2有节点 } if (head1.next != null && head2.next == null) { return head1; //head1有节点,head2无节点 } //定义两个临时链表,方便遍历。 定义一个合并的链表 HeroNode cur1 = head1.next; HeroNode cur2 = head2.next; HeroNode combinationHead = new HeroNode(0,"",""); HeroNode comCur = combinationHead; //对两个链表遍历比较no while (cur1 != null && cur2 != null) { if (cur1.no < cur2.no) { comCur.next = cur1; comCur = comCur.next; cur1 = cur1.next; } else if (cur1.no == cur2.no) { comCur.next = cur1; comCur = comCur.next; //或者 /*combinationHead.next = cur2; combinationHead = combinationHead.next;*/ cur1 = cur1.next; cur2 = cur2.next; } else { comCur.next = cur2; comCur = comCur.next; cur2 = cur2.next; } } //有一个链表比较完了,剩下一个链表直接放在新链表的后面 if (cur1 != null) { comCur.next = cur1; comCur = comCur.next; cur1 = cur1.next; } if (cur2 != null) { comCur.next = cur2; comCur = comCur.next; cur2 = cur2.next; } return combinationHead; }...}//定义HeroManagement,管理我们的英雄class HeroManagement { //初始化头节点,头结点不要动,不存放数据 private HeroNode head = new HeroNode(0,"",""); public HeroNode getHead() { return head; }...}//定义HeroNode,每一个HeroNode 对象就是一个节点class HeroNode {...}
测试结果:
双链表
1) 遍历: 方法和单链表一样,只是可以向前,也可以向后
2) 添加: (默认添加到双向链表的最后)
- (1) 先找到双向链表的最后这个节点
- (2) temp.next = newHeroNode
- (3) newHeroNode.pre = temp
3) 修改:思路和原理和单向链表一样
4) 删除
- (1) 因为是双向链表,因此,我们可以实现自我删除某个节点
- (2) 直接找到要删除的的这个节点,比如temp
- (3) temp.pre.next = temp.next
- (4) temp.next.pre = temp.pre
代码:
package com.jiao.dataStructure;public class DoubleLinkedList { public static void main(String[] args) { //先创建节点数据 HeroNode2 heroNode1 = new HeroNode2(1,"宋江","及时雨"); HeroNode2 heroNode2 = new HeroNode2(2,"鲁智深","花和尚"); HeroNode2 heroNode3 = new HeroNode2(3,"李逵","黑旋风"); HeroNode2 heroNode4 = new HeroNode2(4,"林冲","豹子头"); HeroNode2 heroNode5 = new HeroNode2(5,"公孙胜","入云龙"); //创建双向链表对象 HeroManagement2 heroes = new HeroManagement2(); //添加无序 heroes.add(heroNode3); heroes.add(heroNode1); heroes.add(heroNode4); heroes.add(heroNode2); //遍历 System.out.println("添加后遍历:"); heroes.list(); //修改 HeroNode2 changeHeroNode = new HeroNode2(3,"李逵111","黑旋风111"); heroes.update(changeHeroNode); System.out.printf("修改no=%d后遍历:\n",changeHeroNode.no); heroes.list(); //删除 heroes.del(3); heroes.del(1); System.out.println("删除1,3号后遍历:"); heroes.list(); }}//定义HeroManagement,管理我们的英雄class HeroManagement2 { //初始化头节点,头结点不要动,不存放数据 private HeroNode2 head = new HeroNode2(0, "", ""); public HeroNode2 getHead() { return head; } //1.遍历 public void list() { //判断链表是否为空 if (head.next == null) { System.out.println("链表为空~~~"); return; } //辅助temp来遍历 HeroNode2 temp = head.next; while (true) { if (temp == null) { //判断链表到最后 break; } System.out.println(temp); //打印 temp = temp.next; //指向下一个节点 } } //2.1添加 思路(不考虑编号顺序):添加到最后一个节点的后面 public void add(HeroNode2 heroNode) { //head节点不能动,用辅助遍历temp HeroNode2 temp = head; //临时存放头节点 while (true) { if (temp.next == null) { //找到链表的最后 break; } temp = temp.next; //没找到temp后移 } temp.next = heroNode; //新的节点地址给最后一个节点的next heroNode.pre = temp; //最后节点的地址给新节点的pre } //3.修改 public void update(HeroNode2 newHeroNode) { if (head.next == null) { //判断是否为空 System.out.println("链表为空~"); return; } HeroNode2 temp = head.next; //辅助变量 boolean flag = false; //标志是否找到 while (true) { if (temp.next == null) { //到了链表最后,没有找到同 no break; } if (temp.no == newHeroNode.no) { //找到no相同的temp flag = true; break; } temp = temp.next; } if (flag) { //找到了 temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; } else { //没找到 System.out.printf("没有找到要修改的no=%d的节点。",newHeroNode.no); } } //4.删除 public void del(int no){ //判断为空 if (head.next == null) { System.out.println("链表为空,不能删除~"); return; } HeroNode2 temp = head.next; //辅助变量 boolean flag = false; //标志是否找到 while (true){ if (temp ==null){ //到链表最后也没找到 break; } if (temp.no == no){ //找到了,temp是找到的前一个节点 flag = true; break; } temp = temp.next; } if (flag){ temp.pre.next = temp.next; //next if (temp.next != null) { //pre temp.next.pre = temp.pre; //假如删除的是最后一个节点 temp.next == null } } else { System.out.printf("没有找到要删除的节点no=%d。\n",no); } }}class HeroNode2 { //变量 public int no; //英雄编号 public String name; //英雄名字 public String nickname; //英雄昵称 public HeroNode2 next; //下一个节点地址 public HeroNode2 pre; //前一个节点地址 //构造器 public HeroNode2(int no, String name, String nickname) { this.no = no; this.name = name; this.nickname = nickname; } //为了显示方便,重新toString @Override public String toString() { return "HeroNode2{" + "no=" + no + ", name='" + name + '\'' + ", nickname='" + nickname + '\'' + '}'; }}
测试结果:
5) 作业:添加(按照编号添加)
代码:
package com.jiao.dataStructure;public class DoubleLinkedList { public static void main(String[] args) { //先创建节点数据 HeroNode2 heroNode1 = new HeroNode2(1,"宋江","及时雨"); HeroNode2 heroNode2 = new HeroNode2(2,"鲁智深","花和尚"); HeroNode2 heroNode3 = new HeroNode2(3,"李逵","黑旋风"); HeroNode2 heroNode4 = new HeroNode2(4,"林冲","豹子头"); HeroNode2 heroNode5 = new HeroNode2(5,"公孙胜","入云龙"); //创建双向链表对象 HeroManagement2 heroes = new HeroManagement2(); //添加有序 heroes.addByOrder(heroNode3); heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode4); heroes.addByOrder(heroNode2); heroes.addByOrder(heroNode1); heroes.addByOrder(heroNode5); //遍历 System.out.println("添加后遍历:"); heroes.list(); //修改 HeroNode2 changeHeroNode = new HeroNode2(3,"李逵111","黑旋风111"); heroes.update(changeHeroNode); System.out.printf("修改no=%d后遍历:\n",changeHeroNode.no); heroes.list(); //删除 heroes.del(3); heroes.del(1); System.out.println("删除1,3号后遍历:"); heroes.list(); }}//定义HeroManagement,管理我们的英雄class HeroManagement2 { //初始化头节点,头结点不要动,不存放数据 private HeroNode2 head = new HeroNode2(0, "", ""); public HeroNode2 getHead() { return head; }... //2.2添加 按照编号顺序 public void addByOrder(HeroNode2 heroNode) { HeroNode2 temp = head; //临时变量存放头指针 HeroNode2 next = null; //存放temp的下一个节点 boolean flag = false; //是否重复的标志 while (true) { if (temp.next == null) { //找到了最后 break; } if (temp.next.no > heroNode.no) { //temp.next的no开始大于要插入的no,说明当前的temp找到了 break; } else if ( temp.next.no == heroNode.no) { //要添加的no在链表存在了 flag = true; break; } temp = temp.next; } //2.通过flag,判定添加 if (flag) { System.out.printf("准备插入的英雄编号:%d已经存在,不能添加!\n",heroNode.no); } else { next = temp.next; //改变temp前先保存.next值 //连接前一个 temp.next = heroNode; heroNode.pre = temp; //连接后一个 heroNode.next = next; if (next != null) { next.pre = heroNode; } } }}class HeroNode2 {...}
测试结果:
单向环形链表(Josephu约瑟夫问题)
Josephu(约瑟夫)问题:
设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数, 数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推, 直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
假设:n=5有五个人,k=1从第一个开始报数,m=2数两下, 出队列的顺序:2->4->1->5->3
思路:
构建一个单向的环形链表思路:
- 1) 先创建第一个节点,让first指向该节点,并形成环状;
- 2) 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中。
遍历环形链表:
- 1) 先让一个辅助指针(变量)curBoy,指向first节点;
- 2) 然后通过一个while循环遍历 该环形链表即可curBoy.next=first。
代码段:
package com.jiao.dataStructure;public class Josephu { public static void main(String[] args) { CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); //新建5个节点 circleSingleLinkedList.addBoy(5); //输出节点 circleSingleLinkedList.showBoy(); }}//创建一个单向环形链表class CircleSingleLinkedList { //创建一个first节点,无编号 Boy first = new Boy(-1); //添加节点,构建一个环形的链表,nums表示添加节点个数 public void addBoy(int nums) { //nums数据校验 if (nums < 1){//至少添加1个节点 System.out.println("添加的节点个数不对~"); return; } //curBoy辅助指针,帮助我们构建环形链表 Boy curBoy = null; //for循环来创建我们的环形链表 for (int i = 1; i <= nums; i++) { //根据编号创建节点 Boy boy = new Boy(i); //编号为1的节点 if (i == 1) { first = boy; //给新建的first节点赋值 first.setNext(first); //自己指向自己 curBoy = first; // 变量位置 } else { curBoy.setNext(boy); //curBoy.next指向新加的boy boy.setNext(first); //新加的boy指向first节点 curBoy = boy; //curBoy 后移 } } } //遍历当前的环形链表 public void showBoy() { //判断是否为空 if (first == null) { System.out.println("当前环形链表没有节点~"); return; } //因为first不能动,所以变量curBoy辅助指针 Boy curBoy = first; while (true) { System.out.printf("节点的编号为:%d\n",curBoy.getNo()); if (curBoy.getNext() == first) { //遍历完了 break; } curBoy = curBoy.getNext(); //curBoy后移 } }}//创建一个Boy类,表示一个节点class Boy { private int no; //编号 private Boy next; //指向下一个节点,默认null public Boy(int no) { this.no = no; }...geter和seter省略}
测试结果:
节点出圈:
- 1) 需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点 。 补充:报数前,先让first和helper移动k-1次
- 2) 当报数的时候,让first和helper指针同时的移动m-1次[自己要算一次]
- 3) 这时就可以将first指向的小孩出圈。 first = first.next。 helper.next = first。 原来的first指向的节点就没有任何引用,就会被回收
package com.jiao.dataStructure;public class Josephu { public static void main(String[] args) { CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList(); //新建5个节点 circleSingleLinkedList.addBoy(5); //出圈 circleSingleLinkedList.countBoy(1,2,5); }}//创建一个单向环形链表class CircleSingleLinkedList { //创建一个first节点,无编号 Boy first = new Boy(-1);... //根据用户的输入,计算出小孩出圈的顺序 /** * * @param startNo 表示从第几个小孩开始数数 * @param countNum 表示表示数几下 * @param nums 表示最初有几个小孩在圈里 */ public void countBoy(int startNo, int countNum, int nums) { //先对数据进行校验 if (first == null || startNo < 1 || startNo > nums) { System.out.println("参数输入有误~"); return; } //创建辅助指针,帮助出圈helper Boy helper = first; while (true) { if (helper.getNext() == first) { break; } helper = helper.getNext(); } //移动到指定位置k-1次 for (int i = 0; i < startNo - 1; i++) { first = first.getNext(); helper = helper.getNext(); } //出圈(先移动再出圈) while (true) { if (helper == first) { //最后一个退出 break; } //类似于移动的"步长" for (int j = 0; j < countNum - 1; j++) { first = first.getNext(); //当前要删除的节点 helper = helper.getNext(); //当前要删除节点的前一个节点 } System.out.printf("当前出圈的序号是:%d\n",first.getNo()); //删除当前节点 first = first.getNext(); //当前节点后移一位 helper.setNext(first); //当前要删除的节点的前一个节点(helper)指向当前删除的节点的后一个节点 } System.out.printf("最后一个出圈的的序号是:%d",first.getNo()); //System.out.printf("最后一个出圈的的序号是:%d",helper.getNo()); 一样的 }}//创建一个Boy类,表示一个节点class Boy {...}
测试结果:
发表评论
最新留言
关于作者
