从这周开始学习数据结构与算法
首先老规矩先分享一下视频来源,又又又是尚硅谷的一位老师。废话还能是谁?
b站视频连接:https://www.bilibili.com/video/BV1E4411H73v
OK,话不多说开整
这里是一个目录
一、数据结构与算法概述
1.数据结构和算法的关系
一堆废话:
1) 数据结构(structure)是一门研究组织数据方式的学科:学好数据结构→漂亮并效率高的代码。
2) 要学习好数据结构:用程序去实现解决生活中遇到的问题。
3) 程序 = 数据结构 + 算法
4) 数据结构是算法的基础, 换言之,想要学好算法,需要把数据结构学到位。
上面说了一堆,其实就是下图的关系–>
2.数据结构
数据结构包括:线性结构和非线性结构。
2.1线性结构
1)特点:数据元素之间存在一对一的线性关系
2)两种不同的存储结构:顺序存储结构和链式存储结构。
3)链式存储的线性表称为链表
4)线性结构常见的有:数组、队列、链表和栈。
2.2非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构 ,AND 没了。。。。看着简单可能很难。
二、稀疏数组和队列
他来了她来了,数据结构的学习就从稀疏数组开始吧~~
1.稀疏数组
1.1实际需求
老师上课讲的是五子棋的例子,棋盘实际上就是一个二维数组,很多值默认都为0,因此记录了很多没有意义的值。
棋盘:
对应的原始数组:
因此我们需要将原始数组转换为稀疏数组,当然也就需要将稀疏数组转变为原始数组。
1.2思路分析及代码
1.2.1原始数组—>稀疏数组
1)思路分析
稀疏数组需要存储:
①原始数组的行数和列数
②遍历原始数组,得到非零值以及其坐标
2)上述原始数组的稀疏数组:
其中:稀疏数组共有3行3列,
第一行:表示原始数组大小为11×11,val表示原始数组中非零值的个数
第二行和第三行:表示非零值的坐标以及其对应的值
3)代码如下:
//1.先遍历二维数组得到非0数据的个数
int sum = 0;
for(int i = 0;i < 11;i++){
for(int j = 0;j < 11;j++){
if(chessArr1[i][j] != 0){
sum++;
}
}
}
//2.创建对应的稀疏数组
int sparseArr[][] = new int[sum + 1][3];
//给稀疏数组赋值
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum;
//遍历二维数组,将非0的值存放到sparseArr中
int count = 0;//count 用于记录第几个非0数据
for(int i = 0;i < 11;i++){
for(int j = 0;j < 11;j++){
if(chessArr1[i][j] != 0){
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr1[i][j];
}
}
}
1.2.2稀疏数组—>原始数组
经过上面的分析,稀疏数组转原始数组就很简单了。
直接上代码:
//1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
//2.在读取稀疏数组后几行的数据(从第二行开始),并赋给 原始的二维数组 即可.
for(int i = 1;i < sparseArr.length;i++){
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
1.3附加内容
老师上课留了问题,即可以将稀疏数组保存到本地,就用到了IO流的知识。经过我亲自的试验发现用对象流比较简单。
代码如下(示例):
//将稀疏数组保存到本地
oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(sparseArr);
oos.flush();//刷新操作
//使用对象流读取文件
ois = new ObjectInputStream(new FileInputStream(filePath));
int[][] sparseArr = (int[][]) ois.readObject();
2.队列
队列其实就是排队,遵循先入先出的原则,这一点上与先入后出的栈格格不入。
2.1数组模拟队列
2.1.1思路分析
1)几个值的说明
maxSize:数组的最大容量
front:指向队列头部,默认值为-1
rear:指向队列尾部,默认值为-1
2)两种情况
队列空:rear = front
队列满:rear = maxSize - 1
2.1.2代码
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据~");
return;
}
rear++;//让rear后移
arr[rear] = n;
}
//获取队列的数据,出队列
public int getQueue(){
//判断队列是否空
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
front++;//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\n",i,arr[i]);
}
}
2.2数组模拟环形队列
上述队列存在着只能使用一次的缺点,即不能复用,于是出现了环形数组模拟队列。
2.2.1思路分析
1)几个值的说明
maxSize:数组的最大容量
front:指向队列头部,默认值为0,其实指向队列的第一个元素
rear:指向队列尾部,默认值为0,指向队列最后一个元素的下一个位置
有效数据个数:(rear + maxSize - front) % maxSize
区别:
主要体现在默认值的改变
front++和rear++是在出队列或入队列前进行还是之后进行
//数组模拟队列:
rear++;//让rear后移
arr[rear] = n;
//数组模拟环形队列
arr[rear] = n;
//将rear后移,这里必须考虑去摸
rear = (rear + 1) % maxSize;
2)两种情况
队列空:rear = front
队列满:(rear + 1) % maxSize = front
2.2.2代码
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据~");
return;
}
//直接将数据加入
arr[rear] = n;
//将rear后移,这里必须考虑去摸
rear = (rear + 1) % maxSize;
}
//获取队列的数据,出队列
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]);
}
}
三、链表
1)链表是有序的列表
2)链表是以节点的方式来存储,是链式存储
2)每个节点包含 data 域, next 域:指向下一个节点
3)链表的各个节点不一定是连续存储
4)链表分带头节点的和没有头节点的
5)头节点不存放具体的数据,只是表示链表的表头
1链表的应用实例
使用带头节点的单向链表实现 –水浒英雄(HeroNode)排行榜管理,完成对英雄人物的增删改查操作:
添加数据:
方式一:直接添加到链表的尾部
while(true){
if(temp.next == null){
break;
}
temp = temp.next;
}
temp.next = heroNode;
方式二:根据排名添加
if(temp.next.no > heroNode.no){
heroNode.next = temp.next;
temp.next = heroNode;
}
修改数据:根据no编号来修改
if(temp.no == newHeroNode.no){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}
删除数据:找到要删除节点的上一节点,然后将这个节点的next值指向要删除节点的next。
temp.next = temp.next.next;
2.单链表面试题
1)查找单链表中的倒数第k个结点
先得到链表中数据的个数
然后遍历数组到指定的索引处即可
public static HeroNode findLastIndexNode(HeroNode head,int index){
//判断如果链表为空,返回null
if(head.next == null){
return null;//没找到
}
//第一次遍历得到链表的长度(节点个数)
int size = getLength(head);
//第二次遍历size - index位置,就是我们倒数的第k个节点
//先做一个index的效验
if(index <= 0 || index > size){
return null;
}
//定义辅助变量,for循环定位到倒数的index
HeroNode cur = head.next;//3 //3 - 1 = 2
for(int i = 0;i < size - index;i++){
cur = cur.next;
}
return cur;
}
2)单链表的反转
先新new一个新的头节点,然后依次将数据插入,关键在于数据每次都插入到新的头节点的下一个节点。
public static void reverseList(HeroNode head){
//如果当前链表为空,或者只一个节点,无需反转,直接返回
if(head.next == null || head.next.next == null){
return;
}
//定义一个辅助的指针(变量),帮助我们遍历原来的链表
HeroNode cur = head.next;
HeroNode next = null;//指向当前节点[cur]的下一个节点
HeroNode reverseHead = new HeroNode(0,"","");
//遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
while(cur != null){
next = cur.next;//暂时保存当前节点的下一个节点,因为后面需要使用
cur.next = reverseHead.next;//将cur的下一个节点指向新的链表的最前端
reverseHead.next = cur;//将cur连接到新的链表中
cur = next;//让cur后移
}
//将head.next指向reverseHead.next,实现单链表的反转
head.next = reverseHead.next;
}
3.双向链表
1)双向链表与单向链表在定义上的区别在于:
当前节点既可以通过pre指向前一个节点,又可以通过next指向下一个
2)完成对英雄人物的增删改查操作:
添加数据:
方式一:直接添加到链表的尾部
//先找到链表的尾部
while(true){
if(temp.next == null){
break;
}
temp = temp.next;
}
//然后
temp.next = heroNode;
heroNode.pre = temp;
方式二:根据排名添加
//与单向链表的区别是:不需要借助上一个节点找到
if(temp.no > heroNode.no){
heroNode.next = temp;
heroNode.pre = temp.pre;
temp.pre.next = heroNode;
temp.pre = heroNode;
}
修改数据:根据no编号来修改
//与单链表一致
if(temp.no == newHeroNode.no){
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
}
删除数据:找到要删除节点
//与单链表不同的是,可以直接定位到待删除的节点
temp.pre.next = temp.next;
//如果是最后一个节点,就不要执行下面这句话,否则出现空指针
if(temp.next != null){
temp.next.pre = temp.pre;
}
4.单向环形链表
4.1单向环形链表:
1)思路分析
先创建第一个节点first,并且first.next = first;
然后继续创建剩下的节点,加入到已有的环形链表中即可
2)代码
for(int i = 1;i <= nums;i++){
//根据编号,创建节点
Boy boy = new Boy(i);
//如果是第一个小孩
if(i == 1){
first = boy;
first.setNext(first);//构成环
curBoy = first;//让curBoy指向第一个节点
}else{
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
4.2Josephu(约瑟夫、约瑟夫环)问题
设编号为1,2,… n的n个人围坐一圈,约定编号为k的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
1)思路分析
创建一个helper指向链表最后的节点,即helper.next = first;
然后helper与first同时移动。
2)代码
public void countBoy(int startNo,int countNum,int nums){
//创建一个辅助指针,帮助小孩出圈
Boy helper = first;
//需要创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点
while(true){
if(helper.getNext() == first){//说明helper指向最后小孩节点
break;
}
helper = helper.getNext();
}
//小孩报数前,先让 first和 helper移动 k-1次
for(int i = 0;i < startNo - 1;i++){
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让first和helper指针同时移动m-1此,然后出圈
//这里是一个循环操作,直到圈中只有一个节点
while(true){
if(helper == first){//说明圈中只一个节点
break;
}
//让first和helper指针同时的移动countNum - 1
for(int i = 0;i < countNum - 1;i++){
first = first.getNext();
helper = helper.getNext();
}
//这是first指向的节点,就是要出圈的节点
System.out.printf("小孩%d出圈\n",first.getNo());
//这时将first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);//
}
System.out.printf("最后留在圈中的小孩编号%d\n",helper.getNo());
}
总结
To be continued~~,栈,递归,排序