一. 分类
数据结构包括线性结构和非线性结构。
线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素直接存在一对一的线性关系。
- 线性结构有两种不同的存储结构,即顺序存储结构和链式存储结构。
- 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。
- 链式存储的线性表称为链表,链表中存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。
- 线性结构包括:数组,队列,链表和栈。
非线性结构
非线性结构包括:二维数组,多维数组,广义表,数结构,图结构。
二、稀疏数组(Sparse Array)
经典的棋盘存取问题
一个棋盘,通过横坐标和纵坐标可以唯一的确定棋盘上的点,通过给棋子不同的值,可以将棋子分为不同的阵营。所以当需要存取时,我们可以通过二维数组来保存这个棋盘。
int[][] checkerboard = new int[15][15];
checkerboard[4][5] = 1;
checkerboard[4][6] = 1;
checkerboard[5][6] = 2;
checkerboard[7][5] = 2;
checkerboard[7][7] = 1;
可以发现,二维数组虽然解决了这个需求,但是造成了较多的空间浪费,所以使用稀疏数组来存储。
稀疏数组
当一个数组中大部分元素是0,或者是一个相同的值,或者分布不是很紧密时,可以使用稀疏数组来保存该数组。
稀疏数组的处理方式为:
- 稀疏数组第0行,记录原数组一共有几行几列以及不同值的数量。
- 其余每一行存储每个点位的信息。
将上面的二维数组转为稀疏数组存储为如下所示。
int[][] checkerboard = new int[6][3];
checkerboard[0][0] = 15;
checkerboard[0][1] = 15;
checkerboard[0][2] = 5;
checkerboard[1][0] = 4;
checkerboard[1][1] = 5;
checkerboard[1][2] = 1;
checkerboard[2][0] = 4;
checkerboard[2][1] = 6;
checkerboard[2][2] = 1;
checkerboard[3][0] = 5;
checkerboard[3][1] = 6;
checkerboard[3][2] = 2;
checkerboard[4][0] = 7;
checkerboard[4][1] = 5;
checkerboard[4][2] = 2;
checkerboard[5][0] = 7;
checkerboard[5][1] = 7;
checkerboard[5][2] = 1;
三、队列
队列是一种特殊的线性表,特殊在它只允许表的前端(front,队头)进行删除操作,在表的后端(rear,队尾)进行插入操作。最早进入队列的元素最先从队列删除,故又称队列为先进先出(FIFO)线性表。
顺序队列
顺序队列结构采用一片连续的存储空间,并设置两个指针管理。头指针front指向队头元素,尾指针rear,指向下一个入队元素的位置。
操作规则为:只有队尾能插入元素,每插入一个元素rear增1;只有队头能删除一个元素,front增1。当front = rear时,队列中没有任何一个元素,为空队列。当rear大于数组长度时,队列无法再插入新元素。
溢出现象:当队列为空时,做出队运算产生下溢现象。当队列满时,做进队操作,产生真上溢现象。当队列没满,但是头指针只增不减,超出上限时,产生假上溢现象。
顺序队列存在的问题:
顺序队列被删掉的元素位置无法在被使用,头指针值增不减无法复用空间,造成空间浪费。
循环队列
无论插入或删除,一旦rear或front指针增1时超过了所分配的队列空间,就让它指向起始位置。
在循环队列中,当队列为空时,front=rear。当队列满时,front=rear。为了区别这两种情况,规定循环队列最多只能有MaxSize-1个队列元素,当循环队列中只剩下一个空存储时,队列就已经满了。
- 循环队列的判空条件为front=rear
- 判满条件为front = (rear + 1) % MaxSize
当尾指针指向头指针前一个地址时,队列就满了。所有rear + 1就是头指针的下标,%MaxSize还是头指针的下标。 - 队列有效数据长度为(rear-front + MaxSize) % MaxSize
循环队列实现代码
class CircleArray{
private int maxSize; // 表示数组的最大容量
private int front;
private int rear;
private int[] arr; // 用于存放数据
public CircleArray(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[maxSize];
front = 0;
rear = 0;
}
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;
}
arr[rear] = n;
rear = (rear + 1)%maxSize;
}
public int getQueue(int n) {
if (isEmpty()) {
System.out.println("队列为空")
return;
}
int temp = arr[front];
front = (front + 1)%maxSize;
return temp ;
}
public void showQueue(){
if (isEmpty()) {
System.out.println("队列为空")
return;
}
for (int i = front;i < front + size();i++) {
System.out.printf("arr[%d] = %d\n",i%maxSize,arr[i%maxSize])
}
}
public int headQueue(){
if (isEmpty()) {
System.out.println("队列为空")
return;
}
return arr[front];
}
}
四、链表
- 链表在内存中不是连续的,而是通过指针将一系列不连续的内存联系起来。
- 链表是由结点构成。每个结点包含data域和next域。
- 链表的插入删除元素相较数组较为简单,不需要移动元素,且较为容易实现长度扩充。但是寻找某个元素较为困难。
- 链表分为带头结点的链表和没有头结点的链表,根据实际需求来确定。
1. 单向链表
单向链表的结点被分成两部分,一部分是保存或显示关于节点的信息的信息域,第二部分存储下一个节点地址的指针域,而最后一个节点则指向一个空值。
单向链表中有一个head指针,称为表头结点,指向第一个节点。
2. 单向链表练习题
- 求单链表中有效结点的个数。
// head为头结点
public int length(Node head) {
if (head.next == null) {
return 0;
}
int length = 0;
CurrentNode cur = head.next;
while (cur != null) {
length++;
cur = cur.next;
}
return length;
}
- 查找单链表中的倒数第k个结点
/*
① 求出单链表的长度
② 得出正数第temp结点
③ 取出第temp结点
*/
public Node findNode(Node head,int num) {
if (head.next == null) {
return null;
}
int length = 0;
CurrentNode cur = head.next;
while (cur != null) {
length++;
cur = cur.next;
}
int temp = length - num;
if (temp <= 0 || temp > length) {
return null;
}
cur = head.next;
for (int i = 0; i < temp; i++) {
cur = cur.next;
}
return cur;
}
- 单链表的反转
public void reverse(Node head) {
if (head.next == null || head.next.next == null) {
return ;
}
Node revNode = new Node();
Node temp = null;
CurrentNode cur = head.next;
while (cur != null) {
temp = cur.next;
cur.next = revNode.next;
revNode.next = cur ;
cur = temp;
}
head.next = revNode.next;
}
- 从头到尾打印单链表
方式一:先将单链表进行反转操作,然后再遍历即可。这样做的问题是会破坏原来单链表的结构,不建议。
方式二: 压栈出栈 - 合并两个有序单链表,合并之后的链表依然有序。
3. 双向链表
每个数据节点都有两个指针,分别指向直接后继和直接前驱。所以双向链表任意一个节点开始,都可以很方便地访问前驱后继。
4. 环形链表
链表是单向链表,并且最后一个节点next指针指向了第一个节点,这种链表结构就是单向环形链表,用于解决约瑟夫问题。同理也有双向环形列表
一般我们创建的是没有头结点的单向环形链表,但是会有头指针。
头指针:
- 头指针是指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所以头指针冠以链表的名字(指针变量的名字)
- 无论链表是否为空,头指针均不为空
- 头指针是链表的必要元素
头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(但也可以用来存放链表的长度)
- 有了头结点,对在第一元素结点前插入结点和删除第一结点起操作与其它结点的操作就统一了
- 头结点不一定是链表的必要元素
头结点我们需要创建一个head结点,然后head.next指向第一个元素。头结点在链表当中。
头指针不需要创建一个单独的节点,直接指向第一个元素。头指针不在当前链表中。
5. 约瑟夫问题
Josephu问题:
设编号为1,2,3,…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,他的下一位又从1开始报数,数到m的那个人又出列,以此类推,直到所有人出列位置,由此产生一个出队的编号序列。
public class Josephu {
public static void main(String[] args) {
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.create(5);
circleSingleLinkedList.show();
circleSingleLinkedList.count(1,2,5);
}
}
class CircleSingleLinkedList {
// 头指针
private JosephuNode first = null;
// 根据链表长度创造环形链表
public void create (int nums) {
if (nums < 1) {
System.out.println("nums的值不正确");
}
JosephuNode cur = null;
for (int i = 1; i <= nums; i++) {
// 创造新结点
JosephuNode node = new JosephuNode(i);
if (i == 1) {
// 链表为空
first = node;
first.setNext(node);
cur = first;
} else {
// 链表不为空
cur.setNext(node);
node.setNext(first);
cur = node;
}
}
}
public void show () {
if (first == null) {
System.out.println("链表为空");
}
JosephuNode cur = first;
while (true) {
System.out.println("结点的编号为:" + cur.getNo());
if (cur.getNext() == first) {
break;
}
cur = cur.getNext();
}
}
/**
* 解决约瑟夫问题方法
* @param startNo 开始叫号的编号
* @param step 叫几个号
* @param size 队列中有多少个元素
*/
public void count(int startNo , int step, int size) {
if (first == null || startNo < 1 || startNo > size) {
System.out.println("输入参数有问题");
}
JosephuNode pre = null;
JosephuNode cur = first;
while (true) {
if (cur.getNext() == first) {
pre = cur;
break;
}
cur = cur.getNext();
}
// pre永远是first元素的前一个元素,first元素指向被叫到的元素
for (int i = 0; i < startNo - 1; i++) {
first = first.getNext();
pre = pre.getNext();
}
while (true) {
if (pre == first) {
break; // 当只有一个节点时,pre=first
}
for (int i = 0; i < step - 1; i++) {
first = first.getNext();
pre = pre.getNext();
}
System.out.println("出列的元素为:" + first.getNo());
first = first.getNext();
pre.setNext(first);
}
System.out.println("最后一个元素为:" + first.getNo());
}
}
class JosephuNode {
private int no;
private JosephuNode next;
public JosephuNode(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public JosephuNode getNext() {
return next;
}
public void setNext(JosephuNode next) {
this.next = next;
}
}
五、栈
1.定义
栈的英文为stack,栈是一个先入后出(FILO,first in last out)的有序列表。
栈是限制线性表中元素的插入和删除只能在线性表的同一断进行的一种特殊线性表。允许插入和删除的一段,称为栈顶,另一端为固定的一段,称为栈底。
最先放入栈中的元素在栈底,最后放入的元素在栈顶。
2.数组模拟栈
public class ArrayStack {
public static void main(String[] args) {
ArrayStack arrayStack = new ArrayStack(6);
boolean flag = true;
Scanner scanner = new Scanner(System.in);
while (flag) {
String key = scanner.next();
switch (key) {
case "push" :
int value = scanner.nextInt();
arrayStack.push(value);
break;
case "pop" :
try {
System.out.println(arrayStack.pop());
} catch (RuntimeException e) {
System.out.println("栈已空");
}
break;
case "show" :
arrayStack.show();
break;
case "exit" :
flag = false;
break;
default:
System.out.println("命令输入错误");
break;
}
}
System.out.println("退出成功");
}
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 isFull() {
return top == maxSize - 1;
}
public boolean isEmpty() {
return top == -1;
}
public void push(int value) {
if (isFull()) {
System.out.println("栈已满");
return;
}
stack[++top] = value;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈已空");
}
return stack[top--];
}
public void show() {
if (isEmpty()) {
System.out.println("栈已空");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d] = %d",i,stack[i]);
}
}
}
3.单向链表模拟栈
单向链表一般是有头结点的,应为栈中有栈顶指针。所以我采用无头结点的单向链表,不过有头指针。
public class LinkedListStack {
public static void main(String[] args) {
LinkedListStack linkedListStack = new LinkedListStack(5);
boolean flag = true;
Scanner scanner = new Scanner(System.in);
while (flag) {
String key = scanner.next();
switch (key) {
case "push" :
int value = scanner.nextInt();
linkedListStack.push(value);
break;
case "pop" :
try {
System.out.println(linkedListStack.pop());
} catch (RuntimeException e) {
System.out.println("栈已空");
}
break;
case "show" :
linkedListStack.show();
break;
case "exit" :
flag = false;
break;
default:
System.out.println("命令输入错误");
break;
}
}
System.out.println("退出成功");
}
private LinkedListStackNode first;
private int size;
private int maxSize;
public LinkedListStack(int maxSize) {
this.first = null;
this.size = 0;
this.maxSize = maxSize;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == maxSize;
}
public void push(int num) {
if (isFull()) {
System.out.println("栈已满");
return;
}
LinkedListStackNode newNode = new LinkedListStackNode(num);
newNode.next = first;
first = newNode;
size++;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈已空");
}
int data = first.getData();
first = first.next;
size--;
return data;
}
public int size() {
return size;
}
public void show() {
if (isEmpty()) {
System.out.println("栈已空");
return;
}
LinkedListStackNode cur = first;
while (true) {
System.out.println(cur.getData());
if (cur.getNext() == null) {
break;
}
cur = cur.getNext();
}
}
class LinkedListStackNode {
private int data;
private LinkedListStackNode next;
public LinkedListStackNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public LinkedListStackNode getNext() {
return next;
}
public void setNext(LinkedListStackNode next) {
this.next = next;
}
}
}
4.前缀,中缀,后缀表达式(逆波兰表达式)
- 前缀表达式又称波兰式,前缀表达式的运算符位于操作符之前。
例如:(3+4) * 5-6对应的表达式就是 - X + 3 4 5 6 - 中缀表达式是常见的运算表达式,如(3+4)*5-6
中缀表达式对计算机来说却不好操作。因此在计算结果时,往往会将中缀转成其他表达式来操作(一般转成后缀表达式) - 后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。
例如(3+4)*5-6对应的后缀表达式就是3 4 + 5 * 6 -
5. 中缀转后缀表达式
方法一:
将中缀表达式按照运算顺序加上()。例如:a + b - (c -d) * e,就转为((a + b) - ((c -d)*e))。
如果是转为后缀表达式,就把运算法提到最近括号的右边。如果是前缀表达式,就把运算法提到最近括号的左边。例如((a + b) - ((c -d)*e)),就转为后缀:((ab)+((cd)-e)*)- ,ab+cd-e*-
方法二: