线性结构
线性结构特点
- 数据元素健存在一对一的线性关系
- 有两种不同的存储结构:顺序存储结构和链式存储结构
- 链式存储的线性表成为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素信息以及相邻元素的地址信息
- 线性结构常见类型有:数组,队列,链表,栈
稀疏数组和队列
稀疏数组
-
当一个数组中大部分元素为0,或者为同一个值的数组时,可以用稀疏数组来保存该数组
-
稀疏数组的第一行用来记录该数组共几行几列,并且有多少个不同的值
-
二维数组转稀疏数组
- 遍历原始二维数组,得到有效数据的个数sum
- 根据sun创建稀疏数组sparseArr int [sum+1] [3]
- 将二维数组的有效数据存入到稀疏数组
-
稀疏数组转二维数组
- 先读取稀疏数组的第一行,根据第一行的数据创建原始的二维数组
- 后读取稀疏数组的后几行的数据并赋值给原始的二维数组
-
代码实现
public class SparseArray { public static void main(String[] args) { // 创建初始二维数组 int array[][] = new int[11][11]; array[0][1] = 1; array[1][4] = 2; array[0][7] = 3; array[2][5] = 4; System.out.println("原始二维数组为:"); for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { System.out.printf("%d\t", array[i][j]); } System.out.println(); } // 获取有效数据的个数 int sum = 0; for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { if (array[i][j] != 0) { sum++; } } } // 根据二维数组创建稀疏数组 int sparseArray[][] = new int[sum + 1][3]; // 首元素储存数组的大小和有效数据的个数 sparseArray[0][0] = array.length; sparseArray[0][1] = array[0].length; sparseArray[0][2] = sum; // 遍历原数组并将数据存入稀疏数组 int count = 0; for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { if (array[i][j] != 0) { count++; sparseArray[count][0] = i; sparseArray[count][1] = j; sparseArray[count][2] = array[i][j]; } } } // 输出稀疏数组信息 System.out.println("稀疏数组:"); for (int i = 0; i < sparseArray.length; i++) { System.out.printf("%d\t%d\t%d\n", sparseArray[i][0], sparseArray[i][1], sparseArray[i][2]); } // 将稀疏数组转换成二维数组 int sparseToArray[][] = new int[sparseArray[0][0]][sparseArray[0][0]]; for (int i = 1; i < sparseArray.length; i++) { sparseToArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2]; } // 输出转换后数组内容 System.out.println("转换后二维数组为:"); for (int i = 0; i < sparseToArray.length; i++) { for (int j = 0; j < sparseToArray[i].length; j++) { System.out.printf("%d\t", sparseToArray[i][j]); } System.out.println(); } } }
队列
- 队列是一个有序列表,可以用数组或者链表来实现
- 遵循先入先出的原则
- 需要有两个标志变量front和rear来记录队列的前端和后端,front会随着数据的输出而改变,rear会随着数据的输入而改变
//使用数组模拟队列
public class ArrayQueue{
private int maxSize;
private int front;
private int rear;
private int[] queue;
public ArrayQueue(int queueSize) {
maxSize = queueSize;
front = -1;
rear = -1;
queue = new int[maxSize];
}
//判断队列是否满
public boolean isFull() {
return maxSize==rear+1;
}
//判断队列是否为空
public boolean isEmpty() {
return rear==front;
}
//向队列中添加元素
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列已满");
return;
}
rear++;
queue[rear] = n;
}
//从队列中取出一个元素
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
front++;
return queue[front];
}
//显示队列中的元素
public void showQueue() {
for (int i = 0; i < queue.length; i++) {
System.out.printf("queue[%d]=%d\t",i,queue[i]);
}
}
//显示队列的头信息
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return queue[front+1];
}
}
以上使用数组模拟队列存在队列不能复用的问题,为解决这个问题引入环形队列
环形队列
- 环形队列解决上述队列不能复用的问题
- 环形队列中的front和rear的值默认为0,front指向队列的第一个元素,rear指向队列最后一个元素的后一个位置
- 判断队列是否为空的条件仍为 rear==front
- 判断队列是否已满的条件变为 (rear+1)%maxSize==front
//使用数组模拟环形队列
public class CricleQueue{
private int maxSize;
private int front;
private int rear;
private int[] queue;
public CricleQueue(int queueSize) {
maxSize = queueSize;
front = 0;
rear = 0;
queue = new int[maxSize];
}
//判断队列是否满
public boolean isFull() {
return (rear+1)%maxSize==front;
}
//判断队列是否为空
public boolean isEmpty() {
return rear==front;
}
//向队列中添加元素
public void addQueue(int n) {
if (isFull()) {
System.out.println("队列已满");
return;
}
queue[rear] = n;
rear = (rear+1)%maxSize;
}
//从队列中取出一个元素
public int getQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
int n = queue[front];
front = (front+1)%maxSize;
return n;
}
//显示队列中的元素
public void showQueue() {
for (int i = front; i < front + this.size(); i++) {
System.out.printf("queue[%d]=%d\t",i%(maxSize-1),queue[i%maxSize]);
}
}
//显示队列大小
public int size() {
return (rear+maxSize-front)%maxSize;
}
//显示队列的头信息
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空");
}
return queue[front];
}
}
单向链表
- 链表是以节点的方式来储存的
- 链表的每个节点包含了data域和next域
- 链表在内存中不一定是连续存储的
public class SingleLinkedList {
private InfoNode head= new InfoNode();
//添加节点
public void addNode(InfoNode infoNode) {
InfoNode temp = head;
while(true) {
if (temp.next==null) {
break;
}
temp=temp.next;
}
temp.next=infoNode;
}
//展示节点
public void showList() {
//判断头节点的指向是否为空,为空则直接返回
if (head.next==null) {
return;
}
InfoNode temp = head.next;
while(true) {
if (temp==null) {
break;
}
System.out.println(temp);
temp=temp.next;
}
}
//按照顺序添加
public void addByOrder(InfoNode infoNode) {
//判断头节点的指向是否为空,为空则将head.next指向infoNode
if (head.next==null) {
head.next=infoNode;
return;
}
InfoNode temp = head;
boolean flag = false;
while(true) {
//判断下一个节点是否为空,为空则说明没有找到匹配的插入位置,直接插入在最后
if (temp.next==null) {
temp.next=infoNode;
break;
}else if (temp.next.id > infoNode.id) {//若当前节点的下一个节点的id大于插入id,则将要插入的节点插入两者之间
InfoNode tmp = temp.next;
temp.next=infoNode;
infoNode.next=tmp;
break;
}else if (temp.next.id==infoNode.id) {//若有重复id的节点则表示节点已经存在,不能再插入
flag=true;
break;
}
temp=temp.next;
}
if (flag) {
System.out.println("该id已存在,添加失败");
}else {
System.out.println("添加成功!");
}
}
}
注:
- 单向链表在删除节点的时候需要找到需要删除节点的前一个节点temp,并将temp.next指向temp.next.next;
双向链表
- 双向链表既可以正向遍历也可以反向遍历
- 删除节点时只需要找到要删除的节点即可,如temp.next.pre=temp.pre(将删除节点的下一个节点的pre指向删除节点的前一个节点),temp.pre.next=temp.next(将删除节点的前一个节点的next指向删除节点的下一个节点)
- 代码实现
public class DoubleLinkedList {
public DoubleInfoNode head = new DoubleInfoNode();
//添加节点
public void addNode(DoubleInfoNode newNode) {
if (head.next==null) {
head.next=newNode;
newNode.pre=head;
return;
}
DoubleInfoNode temp= head;
while(true) {
if (temp.next==null) {
temp.next=newNode;
newNode.pre=temp;
break;
}
temp=temp.next;
}
}
//删除节点
public void deleteNode(int id) {
if (head.next==null) {
System.out.println("双向链表为空");
return;
}
DoubleInfoNode temp= head;
while(true) {
if (temp.id==id) {
temp.pre.next=temp.next;
if (temp.next!=null) {
temp.next.pre=temp.pre;
}
System.out.println("删除成功");
break;
}
temp=temp.next;
}
}
//按顺序添加节点
public void addByOrder(DoubleInfoNode newNode) {
if (head.next==null) {
head.next=newNode;
newNode.pre=head;
return;
}
DoubleInfoNode temp=head;
boolean flag = false;
while(true) {
if (temp.next==null) {
temp.next=newNode;
newNode.pre=temp;
break;
}else if (temp.next.id > newNode.id) {
newNode.pre=temp;
newNode.next=temp.next;
temp.next=newNode;
newNode.next.pre=newNode;
break;
}else if (temp.next.id==newNode.id) {
flag=true;
break;
}
temp=temp.next;
}
if (flag) {
System.out.println("该id已存在,添加失败");
}else {
System.out.println("添加成功!");
}
}
//遍历节点
public void showList() {
if (head.next==null) {
return;
}
DoubleInfoNode temp = head.next;
while(true) {
if (temp==null) {
break;
}
System.out.println(temp);
temp=temp.next;
}
}
}
环形链表
-
使用一个first指针记录开始节点
-
操作链表时,使用curNode指针记录当前指向节点
-
需要将最后一个节点的next指针指向first
-
遍历时循环的结束条件为curNode.next==first
/** * 创建单向环形链表解决约瑟夫环问题 * @author dell * */ public class CircleSingleLinkedList { private InfoNode first = null; //构建节点数为num的单向环形链表 public void addNode(int num) { if (num<1) { System.out.println("添加的节点必须大于等于1"); return; } InfoNode curNode = null; for (int i = 1; i <= num; i++) { InfoNode node = new InfoNode(i,""); if (i==1) { first=node; first.next=first; curNode=first; }else { curNode.next=node; node.next=first; curNode=node; } } } //显示当前单向环形列表的信息 public void showList() { if (first==null) { System.out.println("链表为空"); return; } InfoNode curNode = first; while(true) { System.out.println(curNode); if (curNode.next==first) { break; } curNode=curNode.next; } } }
使用单向环形列表解决约瑟夫环问题
//约瑟夫问题解决 public void countNode(int startNum,int countNum,int nodeNum) { if(startNum<1||countNum<1||nodeNum<1||startNum>nodeNum) { System.out.println("数据输入不正确"); return; } addNode(nodeNum); //定义helpNode并使helpNode指向最后一个节点 InfoNode helpNode = first; while(true) { if (helpNode.next==first) { break; } helpNode=helpNode.next; } //将helpNode和first移动num-1个单位来满足题设 for (int i = 0; i < startNum-1; i++) { helpNode=helpNode.next; first=first.next; } while(true) { if (helpNode==first) { break; } //移动指针(数数过程) for (int i = 0; i < countNum-1; i++) { helpNode=helpNode.next; first=first.next; } System.out.println(first); first=first.next; helpNode.next=first; } System.out.println(helpNode); }
栈
- 栈的特点为先入后出,后入先出
- 栈可以用来存储计算机执行的指令,计算数学表达式的值(需要由中缀表达式转为后缀表达式)
- 栈的实现:初始化top指针为-1,使用top指针指向栈顶
public class ArrayStack {
private int maxSize;
private int top;
private int[] stack;
//构造函数初始化属性值
public ArrayStack(int maxSize) {
this.maxSize=maxSize;
this.stack=new int[maxSize];
this.top=-1;
}
//判断是否为空
public boolean isEmpty() {
return top==-1;
}
//判断栈是否已满
public boolean isFull() {
return top==maxSize-1;
}
//入栈操作
public void push(int num) {
if (isFull()) {
System.out.println("栈满");
return;
}
this.top++;
this.stack[top]=num;
}
//出栈操作
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈为空");
}
return this.stack[top--];
}
//遍历并输出栈的信息
public void showStack() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = top; i >=0; i--) {
System.out.printf("stack[%d]=%d\n",i,stack[i]);
}
}
}
利用栈完成逆波兰表达式的计算(后缀表达式)
public class PolandNotation {
//将逆波兰表达式的运算字符串截取为String类型的List
private List<String> getStringList(String suffixExpression) {
List<String> list = new ArrayList<>();
String[] split = suffixExpression.split(" ");
for (String elem : split) {
list.add(elem);
}
return list;
}
public int calculate(String suffixExpression) {
//将表达式转为List
List<String> stringList = this.getStringList(suffixExpression);
//声明一个数字栈
Stack<String> numStack = new Stack<>();
for (String elem : stringList) {
//正则匹配:若匹配为数字,则入数字栈
if (elem.matches("\\d+")) {
numStack.push(elem);
}else { //否则为运算符,从栈顶弹出两个元素并进行相应的运算
int num1 = Integer.parseInt(numStack.pop());
int num2 = Integer.parseInt(numStack.pop());
int res = 0;
if (elem.equals("+")) {
res = num1 + num2;
} else if (elem.equals("*")) {
res = num1 * num2;
} else if (elem.equals("-")) {
res = num2 - num1;
} else if (elem.equals("/")) {
res = num2 / num1;
}else {
System.out.println("ERROR!");
}
//运算完成后将结果入数字栈
numStack.push("" + res);
}
}
//最后留在数字栈中的即为表达式运算结果
return Integer.parseInt(numStack.pop());
}
public static void main(String[] args) {
PolandNotation polandNotation = new PolandNotation();
int calculate = polandNotation.calculate("3 4 + 5 * 7 /");
System.out.println(calculate);
}
}
优先队列
- 优先队列是一种基于二叉堆的存储结构。
- 优先队列中可以实现删除并返回最大元素的功能。
- 对于一个含有N个元素的基于堆的优先队列,插入元素操作只需要不超过(lgN + 1)次比较,删除最大元素的操作需要不超过2lgN次比较。
哈希表
- 哈希表中包含一个链表数组,每个链表都包含对链表的增删改查操作。
- 哈希表中通过一个散列函数将不同的特征值元素映射到不同的数组元素中。
- 通过散列函数对不同特征值元素的映射,可以加快数据元素的查找速度。
public class HashTableDemo {
class Emp{
public int id;
public String name;
public Emp next;
public Emp(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Emp{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
class EmpLinkedList{
private Emp head;
public void add(Emp emp) {
if (head == null) {
head = emp;
return;
}
Emp temp = head;
while (true) {
if (temp.next == null) {
temp.next = emp;
break;
}
temp = temp.next;
}
}
public void list() {
Emp temp = head;
while (true) {
if (temp == null) {
break;
}
System.out.println(temp);
temp = temp.next;
}
}
}
class HashTab{
//链表数组,使用哈希散列函数进行不同特征值的映射
EmpLinkedList[] lists;
int size;
public HashTab(int size) {
this.size = size;
this.lists = new EmpLinkedList[size];
//注意此处应该将数组链表中的元素初始化,否则后续操作会出现空指针异常
for (int i = 0; i < size; i++) {
lists[i] = new EmpLinkedList();
}
}
public void add(Emp emp) {
if (emp == null) {
return;
}
int listNum = hashFun(emp.id);
lists[listNum].add(emp);
}
public void list() {
for (int i = 0; i < lists.length; i++) {
lists[i].list();
}
}
//这里散列函数为演示方便为简单的取模运算,决定不同的特征值元素在哪个链表中
public int hashFun(int id) {
return id % size;
}
}
}