数据结构与算法 – 基础(一)
1.什么是数据结构
1.数据结构是一种存储数据的方式,分为线性结构和非线性结构;
2.线性结构:
- 最常用的数据结构,其特点就是数据之间存在一对一的线性关系
- 有顺序存储结构和链式存储结构两种,顺序存储的线性表称为顺序表,顺序表中存储的元素是连续的
- 链式存储的线性表称为链表,链表存储的元素不一定是连续的,元素节点存放了数据元素和相邻元素的地址信息
- 线性结构常见的有数组 、队列 、链表和栈
3.非线性结构有:二维数组 、多维数组 、广义表 、树结构 、图结构
2.稀疏数组和队列
1.稀疏数组
1.应用场景:
2.稀疏数组:当一个数组中大部分元素为0或者为同一个值时可以使用稀疏数组来保存该数组
3.稀疏数组的处理方法:
- 稀疏数组固定有三列,总行数为目标数组中不同值的个数加一(因为稀疏数组是从第二行开始记录不同值的信息的)
- 对于第一行:第一列记录目标数组有多少行,第二列记录目标数组有多少列,第三列记录目标数组有多少个不同值
- 对于从第二行开始,第一列记录某个不同值的行数,第二列记录某个不同值的列数,第三列记录某个不同值的数值大小
如下图:
3.代码实现:
//以上面的五子棋为应用场景
// 普通二维数组转稀疏数组
public int[][] parseToSparseArray(int[][] array) {
//1.根据目标数组定义稀疏数组
int[][] sparseArr = new int[array.length + 1][3]; // array.length 指的是二位数组的行数
//2.计算出目标数组中不同值的个数
int sum = 0;
for(int[] row : array) {
for(int data : row) {
if(data != 0) {
sum ++;
}
}
}
//3.给稀疏数组的第一行赋值
sparseArr[0][0] = array.length;
sparseArr[0][1] = array[0].length; // array[0].length 指的是二维数组的列数
sparseArr[0][2] = sum + 1;
//4.遍历二维数组给稀疏数组的其他行赋值
int k = 1;
for(int i = 0; i < array.length; i++) {
for(int j = 0; j < array[0].length; j++) {
if(array[i][j] != 0) {
sparseArr[k++][0] = i;
sparseArr[k++][1] = j;
sparseArr[k++][0] = array[i][j];
}
}
}
return sparseArr;
}
//稀疏数组转二维数组
public int[][] parseToArray(int[][] sparseArr) {
//1.根据稀疏数组创建二维数组
int[][] arr = new int[sparseArr[0][0]][sparseArr[0][1]];
//2.从稀疏数组的第二行开始遍历给二维数组赋值
for(int i = 1; i < sparseArr.length) {
arr[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
return arr;
}
2.队列
1.应用场景:
2.队列:
- 是一个有序列表,可以使用数组或链表实现
- 遵循先进先出的原则
3.使用环形数组模拟队列思路
- 环形数组是在数组基础上使用取模来实现的
- 有三个变量 front 、rear 、maxSize
- front 指向队列的第一个元素,即初始化值为 0,用于取出元素
- rear 指向队列最后一个元素的后一个元素,即初始化值也为 0,用于添加元素
- maxSize 为数组的初始化大小(即实际数组的长度,而环形数组通过取模实现了无限长度)
- 当 front == rear 时,队列为空
- 当尾索引的下一个为头索引时,队列为满,即 (rear + 1) % maxSize == front
- 队列中有效的元素个数为 (rear + maxSize - front) % maxSize
// 用环形数组实现队列类
class Queue {
private int maxSize;
private int[] arr;
private int front = 0;
private int rear = 0;
//通过构造方法创建数组,并初始化 maxSize 的值
public Queue(int maxSize) {
this.maxSize = maxSize;
arr = new int[maxSize];
}
//判断队列是否为空
public boolean isEmpty() {
return front == rear;
}
//判断队列是否为满
public boolean isFull() {
return (rear + 1) % maxSize == front;
}
//添加数据到队列
public void add(int num) {
if(isFull) {
System.out.println("队列已满,无法添加数据!");
return;
}
//添加数据
arr[rear] = num;
//添加后,rear 向后移,但是要注意取模(在这里实现了环形数组),否则数组会越界
rear = (rear + 1) % maxSize;
}
//获取队列中的数据,与查询不同,取出后逻辑上我们认为该元素在数组中已经不存在了(实际上还是存在,不过会在后续的添加中被其他元素覆盖掉)
public int get() {
if(isEmpty) {
throw new RuntimeException("队列为空,无法取出数据!");
}
int val = arr[front];
//同样要注意取模
front = (front + 1) % maxSize;
return val;
}
//获取该队列中的有效元素个数
public int size() {
return (rear + maxSize - front) % maxSize;
}
//遍历队列
public void showQueue() {
if(isEmpty) {
System.out.println("队列为空,没有数据!");
return;
}
//遍历时从front所指向的元素开始,因为有size()个元素,所以遍历size()次
for(int i = front; i < front + size(); i++) {
//这里要注意取模
System.out.printf("arr[%d]=%d\n",i % maxSize,arr[i % maxSize]);
}
}
//查询队列头元素,不是取出数据
public int getHead() {
if(isEmpty) {
System.out.println("队列为空!");
}
return arr[front];
}
}
3.链表
1.链表是有序的列表,但在内存中的存储如下:
1.单向链表
1.单链表(带表头)的逻辑结构如下:
2.应用实例:使用单向链表完成对水浒英雄的增删改查
思路:
- 直接在链表的尾部添加,先用指针遍历,直至该节点的下一个节点为NULL,那么该节点为链表最后一个节点,然后把要添加的节点设置为该节点的下一个节点
- 按顺序(这里是按英雄的排名)把要添加的节点插入到链表中,先用指针遍历,每遍历一个节点,就拿该节点的排序号(这里是 no)与要添加的节点的排序号比较,若大于后者,则将要添加的节点插入到该节点前面,若两者等于,则输出该节点已存在,并退出添加方法,否则就继续向后一个节点移动,直到找到要插入的位置或者遍历到链表末尾(这种情况直接插入到最后一个节点后面就行)
- 修改节点,先用指针遍历,若遍历的节点的排序号和传入的节点的排序号相等,则找到,并修改节点,否则继续向下遍历直到找到并修改或者**遍历到链表的末尾仍然找不到 **(这种情况就输出未找到节点,并退出方法)
- 删除节点,用指针遍历,若遍历的节点与输入的排序号相等,则删除该节点,否则继续遍历直到找到或者遍历到末尾仍然找不到
- 查询节点,与删除节点类似
//需要先定义一个节点类然后才能实现链表
class HeroNode {
private int no;//排序号
private String name;//英雄名字
private HeroNode next;//每个节点都应该包含有一个节点属性,用来指向下一个节点
public HeroNode() {}
public HeroNode(int no,String name) {
this.no = no;
this.name = name;
}
//这里省略属性的 Getter 、Setter方法和 toString方法
}
//定义链表类
class SingleLinkedList {
private HeroNode head = new HeroNode();//头结点不存储数据,只是用来指向链表第一个节点
private HeroNode temp;//中间变量,操作时需要用到
//1.1 添加方法,直接添加到链表末尾
public void addLast(HeroNode node) {
temp = head;
//先遍历到链表末尾
while(temp.getNext() != null) {
temp = temp.getNext();
}
//在链表末尾添加该节点(若链表为空也会跳到这一步)
temp.setNext(node);
}
//1.2 添加方法,按升序方式添加
public void addByOrder(HeroNode node) {
temp = head;
//若遍历到了链表末尾(包括空链表的情况)则结束循环
while(temp.getNext() != null){
if(temp.getNext().getNo() == node.getNo()) { //排序号在链表中已存在
System.out.println("节点在链表中已存在!");
return;
}else if(temp.getNext().getNo() < node.getNo()) { //排序号比插入的节点小,继续向后遍历
temp = temp.getNext();
}else {
temp.setNext(node);
node.setNext(temp.getNext());
return;
}
}
//遍历到链表末尾
temp.setNext(node);
}
//2.修改节点
public void update(HeroNode newNode) {
temp = head;
while(temp.getNext() != null) {
if(temp.getNext().getNo() == newNode.getNo()) {
//找到了,对节点进行修改
temp.getNext().getName() = newNode.getName();
return; //修改完,退出方法
} else {
temp = temp.getNext();
}
}
//遍历到链表末尾(包括空链表)仍然找不到
System.out.println("要修改的节点不存在!");
}
//3.删除节点
public boolean delete(int no) {
temp = head;
while(temp.getNext() != null){
if(temp.getNext().getNo() == no) {
temp.setNext(temp.getNext().getNext());
return true;
}else{
temp = temp.getNext();
}
}
System.out.println("要删除的节点不存在!");
return false;
}
//4.1 查询单个节点
public HeroNode findByNo(int no) {
temp = head;
while(temp.getNext() != null){
if(temp.getNext().getNo() == no) {
return temp.getNext();
}else{
temp = temp.getNext();
}
}
System.out.println("节点不存在!");
return null;
}
//4.2 遍历链表
public void showList() {
temp = head;
if(temp.getNext() == null) {
System.out.println("链表为空!");
return;
}
while(temp.geNext() != null) {
System.out.println(temp.getNext());
temp = temp.getNext();
}
}
}
2.双向链表
//这里写和单链表有差异的几个方法,像修改,遍历是一样的实现就不写了
// 定义节点类,多出了一个前驱节点
class HeroNode {
private int no;
private String name;
private HeroNode next;
private HeroNode pre; //前驱节点
//其他省略
}
//定义链表类
class DoubleLinkedList {
private HeroNode head = new HeroNode();
private HeroNode temp;
//1.1 直接添加到末尾
public void addLast(HeroNode node) {
temp = head;
//先遍历到链表末尾
while(temp.getNext() != null) {
temp = temp.getNext();
}
//在链表末尾添加该节点(若链表为空也会跳到这一步)
temp.setNext(node);
node.setPre(temp); //多了一步设置前驱节点
}
//1.2 按升序方式添加
public void addByOrder(HeroNode node) {
temp = head;
//若遍历到了链表末尾(包括空链表的情况)则结束循环
while(temp.getNext() != null){
if(temp.getNext().getNo() == node.getNo()) { //排序号在链表中已存在
System.out.println("节点在链表中已存在!");
return;
}else if(temp.getNext().getNo() < node.getNo()) { //排序号比插入的节点小,继续向后遍历
temp = temp.getNext();
}else {
temp.setNext(node);
node.setPre(temp); //多了一步设置前驱节点
node.setNext(temp.getNext());
temp.getNext().setPre(node);//这里不用考虑temp.getNext()为空的情况,因为while循环的条件帮我们保证了temp.getNext()不为空
return;
}
}
//遍历到链表末尾
temp.setNext(node);
node.setPre(temp); //多了一步设置前驱节点
}
//删除
public boolean delete(int no) {
temp = head;
while(temp.getNext() != null){
if(temp.getNext().getNo() == no) { //temp.getNext()是那个要删除的 节点
temp.setNext(temp.getNext().getNext());
if(temp.getNext().getNext() != null) {
temp.getNext().getNext().setPre(temp);
}
return true;
}else{
temp = temp.getNext(); //找不到继续往后遍历
}
}
System.out.println("要删除的节点不存在!");
return false;
}
}
3.单向环形链表
1.应用场景
2.解决思路
//定义一个节点类
class Boy {
private int no;
private Boy next;
public Boy(int no){
this.no = no;
}
//其他省略
}
//定义环形链表类
class CircleSingleLinkedList {
private Boy first = null;
//1. 构造一个环形单向链表
public void addBoy(int nums) { //nums 指的是要创建的环形链表长度
private Boy curBoy = null; //辅助指针,用来帮助构造链表的
//先进行数据校验
if(nums < 1) {
System.out.println("数据不正确!");
return;
}
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; //让curBoy指向链表的最后一个孩子
}
}
}
//2.遍历环形链表
public void showBoy() {
if(first == null) {//先判断链表是否为空
System.out.println("链表为空!");
return;
}
private Boy curBoy = first;
while(curBoy.getNext() != first) { // curBoy.getNext() == first,说明遍历完毕
System.out.printf("小孩的编号 %d\n",curBoy.getNo());
curBoy = curBoy.getNext();
}
}
//3.约瑟夫问题解决
public void countBoy(int startNo.int countNum,int nums) {
//先进行数据验证
if(startNo > nums || startNO < 1) {
System.out.println("输入参数有误!");
return;
}
addBoy(nums);//构造环形链表
//构造辅助指针 helper
private Boy helper = first;
//让 helper 指向链表最后一个节点
while(helper.getNext() != first) {
helper = helper.getNext();
}
//小孩报数前,让 helper 和 first 同时向后移动 startNo - 1步
for(int i = 0; i < startNo - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
//构造一个大循环,直到链表中只剩下一个first节点
//在大循环里面,小孩开始报数,让 helper 和 first 同时向后移动 countNum - 1步,然后first所指向的小孩节点出圈
while(helper != first) {
for(int i = 0; i < countNum - 1; i++) {
first = first.getNext();
helper = helper.getNext();
}
System.out.printf("小孩%d出圈\n",first.getNo());
//first所指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号为%d\n",first.getNo());
}
}