在卷子里考察的会拿红色着重注明。
23-24学年数据结构与算法Ⅲ的期末卷考察点
线性表是由n个数据元素构成的有限序列,其中元素之间存在唯一的前驱和后继关系。线性表可以为空表,也可以包含一个或多个元素。线性表有两种常见的实现方式:顺序表和链表。
插入(Insert): 在线性表的指定位置插入一个新的元素。
void insert(ElementType item, int position);
删除(Delete): 删除线性表中指定位置的元素。
void delete(int position);
查找(Search): 在线性表中查找指定元素的位置。
int search(ElementType item);
遍历(Traverse): 依次访问线性表中的每个元素。
void traverse();
以上的基本操作适用于顺序表和链表两种实现方式。在顺序表中,元素在内存中是连续存储的;而在链表中,元素通过 节点 和 指针 的方式连接在一起,可以是连续的,也可以是分散的。
在实际应用中,选择顺序表还是链表取决于具体的需求。顺序表适用于对元素的随机访问较多的情况,而链表适用于频繁插入和删除操作的情况。
下面是一个简单的链表实现的 Java 代码示例,包括插入、删除和遍历操作:
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
public class LinkedList {
Node head;
// 插入操作
void insert(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
} else {
Node temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = newNode;
}
}
// 删除操作
void delete(int data) {
if (head == null) {
return;
}
if (head.data == data) {
head = head.next;
return;
}
Node temp = head;
while (temp.next != null && temp.next.data != data) {
temp = temp.next;
}
if (temp.next != null) {
temp.next = temp.next.next;
}
}
// 遍历操作
void traverse() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " ");
temp = temp.next;
}
System.out.println();
}
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.insert(1);
list.insert(2);
list.insert(3);
System.out.println("Original List:");
list.traverse();
list.delete(2);
System.out.println("List after deleting element 2:");
list.traverse();
}
}
当使用顺序存储结构(数组)实现线性表时,我们可以通过数组来存储元素,并通过数组的索引来表示元素在线性表中的位置。
插入(Insert): 在指定位置插入一个新的元素。
void insert(int[] array, int size, int position, int element) {
if (position < 0 || position > size) {
System.out.println("Invalid position");
return;
}
// 将插入位置后的元素向后移动一位
for (int i = size - 1; i >= position; i--) {
array[i + 1] = array[i];
}
// 在插入位置插入新元素
array[position] = element;
}
删除(Delete): 删除指定位置的元素。
void delete(int[] array, int size, int position) {
if (position < 0 || position >= size) {
System.out.println("Invalid position");
return;
}
// 将删除位置后的元素向前移动一位
for (int i = position; i < size - 1; i++) {
array[i] = array[i + 1];
}
}
查找(Search): 查找指定元素的位置。
int search(int[] array, int size, int element) {
for (int i = 0; i < size; i++) {
if (array[i] == element) {
return i; // 返回元素的位置
}
}
return -1; // 元素不存在
}
遍历(Traverse): 遍历整个线性表。
void traverse(int[] array, int size) {
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
当使用链式存储结构(链表)实现线性表时,我们可以通过节点之间的指针关系来表示元素的逻辑顺序。链表的基本操作包括插入、删除、查找和遍历。
单向链表中,每个节点包含一个数据域和一个指向下一个节点的指针。
插入(Insert): 在指定位置插入一个新的节点。
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
void insert(Node head, int position, int element) {
Node newNode = new Node(element);
// 找到插入位置的前一个节点
Node temp = head;
for (int i = 0; i < position - 1 && temp != null; i++) {
temp = temp.next;
}
if (temp == null) {
System.out.println("Invalid position");
return;
}
// 插入新节点
newNode.next = temp.next;
temp.next = newNode;
}
删除(Delete): 删除指定位置的节点。
void delete(Node head, int position) {
// 找到删除位置的前一个节点
Node temp = head;
for (int i = 0; i < position - 1 && temp != null; i++) {
temp = temp.next;
}
if (temp == null || temp.next == null) {
System.out.println("Invalid position");
return;
}
// 删除节点
temp.next = temp.next.next;
}
查找(Search): 查找指定元素的位置。
int search(Node head, int element) {
Node temp = head;
int position = 0;
while (temp != null && temp.data != element) {
temp = temp.next;
position++;
}
if (temp == null) {
return -1; // 元素不存在
} else {
return position; // 返回元素的位置
}
}
遍历(Traverse): 遍历整个链表。
void traverse(Node head) {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " ");
temp = temp.next;
}
System.out.println();
}
带头节点的链表中,头节点不存储实际的数据,仅用于标识链表的起始位置。初始化:
package linklist;
public class ListNode {
public int id;
//数据域
public String data;
//指针域
public ListNode next;
public ListNode(int id,String data){
this.id = id;
this.data = data;
}
@Override
public String toString() {
return "ListNode{" +
"id=" + id +
", data='" + data + '\'' +
'}';
}
}
具体实现:
package linklist;
public class SingleLinkList {
//先初始化一个头节点,头节点不存放具体数据
private ListNode head;
public void initSingLinkList() {
head = new ListNode(0, "");
}
//添加节点
public void insertNode(ListNode listNode) {
//使用temp指针进行遍历
ListNode temp = head;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = listNode;
}
//打印链表
public void showSingleLinkList() {
if (isEmpty()) {
return;
}
ListNode temp = head.next;
while (true) {
if (temp == null) {
return;
}
System.out.println(temp);
temp = temp.next;
}
}
//判断链表是否为空
public boolean isEmpty() {
if (head.next == null) {
System.out.println("链表为空");
return true;
}
return false;
}
//按照id顺序添加节点
public void insertByOrder(ListNode listNode) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.id > listNode.id) {
break;
} else if (temp.next.id == listNode.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag == true) {
System.out.println("准备插入的数据编号已存在,无法添加");
} else {
listNode.next = temp.next;
temp.next = listNode;
System.out.println("插入成功");
}
}
//修改节点数据
public void updateNode(ListNode listNode) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.id == listNode.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.data = listNode.data;
} else {
System.out.println("没有找到要修改的节点");
}
}
//删除节点
public void delNode(int id) {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.id == id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("要删除的节点不存在");
}
}
//获取头节点
public void getHeadNode() {
ListNode temp = head;
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法获取头节点");
} else {
temp = temp.next;
System.out.println("头节点为:" + temp);
}
}
//获取尾节点
public void getTailNode() {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法获取尾节点");
break;
}
if (temp.next == null) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("尾节点为:" + temp);
} else {
System.out.println("无法找到该节点");
}
}
//清空链表
public void clearLinkList() {
ListNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空无法清空");
break;
}
temp = temp.next;
temp.data = "";
if (temp.next == null) {
flag = true;
break;
}
}
if (flag) {
System.out.println("清空完成");
}
}
//获取长度
public void getLength() {
ListNode temp = head;
int length = 0;
while (true) {
if (temp.next == null && temp.id == 0) {
System.out.println("该链表为空长度为"+length);
break;
}
temp = temp.next;
length++;
if (temp.next == null) {
System.out.println("该链表为空长度为"+length);
break;
}
}
}
}
一些在选择存储结构时需要考虑的因素:
访问方式:
元素的动态性:
内存空间效率:
插入和删除的复杂性:
存储元素类型的灵活性:
对缓存的利用:
对算法的要求:
在实际选择存储结构时,需要综合考虑这些因素,并根据具体问题的性质权衡它们。在某些情况下,也可以考虑使用一些高级的数据结构,如哈希表、树等,以满足更复杂的需求。
下面是基于Java的简单实现:
import java.util.LinkedList;
// 栈的实现
class Stack {
private LinkedList<Integer> stack;
public Stack() {
stack = new LinkedList<>();
}
public void push(int element) {
stack.addLast(element);
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.removeLast();
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack.getLast();
}
public boolean isEmpty() {
return stack.isEmpty();
}
public int size() {
return stack.size();
}
}
// 队列的实现
class Queue {
private LinkedList<Integer> queue;
public Queue() {
queue = new LinkedList<>();
}
public void enqueue(int element) {
queue.addLast(element);
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue.removeFirst();
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue.getFirst();
}
public boolean isEmpty() {
return queue.isEmpty();
}
public int size() {
return queue.size();
}
}
class ArrayStack {
private int[] stack;
private int top; // 栈顶指针
public ArrayStack(int capacity) {
stack = new int[capacity];
top = -1;
}
public void push(int element) {
if (top == stack.length - 1) {
throw new RuntimeException("Stack is full");
}
stack[++top] = element;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack[top--];
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return stack[top];
}
public boolean isEmpty() {
return top == -1;
}
public int size() {
return top + 1;
}
}
class ArrayQueue {
private int[] queue;
private int front; // 队头指针
private int rear; // 队尾指针
public ArrayQueue(int capacity) {
queue = new int[capacity];
front = rear = -1;
}
public void enqueue(int element) {
if (isEmpty()) {
front = rear = 0;
} else if (rear == queue.length - 1) {
throw new RuntimeException("Queue is full");
} else {
rear++;
}
queue[rear] = element;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = queue[front];
if (front == rear) {
front = rear = -1;
} else {
front++;
}
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue[front];
}
public boolean isEmpty() {
return front == -1 && rear == -1;
}
public int size() {
if (isEmpty()) {
return 0;
}
return rear - front + 1;
}
}
这里的栈和队列基于数组实现,通过维护相应的指针来模拟栈和队列的特性。这种实现方式在一些场景中可能更为直观,但需要注意数组容量的限制。如果数组容量不足,需要进行扩容操作。
链式存储结构对栈和队列的实现同样可以通过指针和节点的方式完成。下面分别是链式存储结构对栈和队列的基本操作实现:
// 节点类
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
// 链式栈的实现
class LinkedStack {
private Node top;
public LinkedStack() {
top = null;
}
public void push(int element) {
Node newNode = new Node(element);
newNode.next = top;
top = newNode;
}
public int pop() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
int element = top.data;
top = top.next;
return element;
}
public int top() {
if (isEmpty()) {
throw new RuntimeException("Stack is empty");
}
return top.data;
}
public boolean isEmpty() {
return top == null;
}
public int size() {
int count = 0;
Node temp = top;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
}
// 链式队列的实现
class LinkedQueue {
private Node front; // 队头指针
private Node rear; // 队尾指针
public LinkedQueue() {
front = rear = null;
}
public void enqueue(int element) {
Node newNode = new Node(element);
if (isEmpty()) {
front = rear = newNode;
} else {
rear.next = newNode;
rear = newNode;
}
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = front.data;
front = front.next;
if (front == null) {
rear = null; // 如果队列为空,重置队尾指针
}
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return front.data;
}
public boolean isEmpty() {
return front == null;
}
public int size() {
int count = 0;
Node temp = front;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
}
这里的链式存储结构实现了栈和队列的基本操作,通过指针的方式实现了元素的入栈、出栈、入队、出队等操作。链式结构相对于数组结构在动态操作方面更为灵活。
循环队列是一种特殊的队列实现,通过使用循环方式维护队列的前后指针,实现循环利用底层数组,避免了队列元素在出队时需要移动整个队列的情况。在循环队列里要约定一个空位置,这是为了更好地区分队列为空和队列为满的情况。在循环队列中,队头指针 front
和队尾指针 rear
在数组中移动时,可能会出现两种情况:
front
和 rear
指针重合时,队列为空。(rear + 1) % capacity == front
时,队列为满。如果没有约定一个空位置,那么在队列为空和队列为满时,front
和 rear
指针都会重合,导致无法准确判断队列的状态。通过约定一个空位置,我们可以确保在队列为满时 rear
的下一个位置一定是 front
,而在队列为空时,front
和 rear
指针重合。这样就能够清晰地判断队列的状态。
具体来说,如果队列的容量是 7,我们初始化数组为 int[8]
,并约定第 8 个位置为空。这样,当 front
和 rear
指针都指向数组的第 0 个位置时,表示队列为空;当 (rear + 1) % capacity == front
时,表示队列为满。
这种约定使得循环队列的状态判断更加清晰,避免了状态混淆。如果队列的容量是 n
,那么实际数组的大小应该是 n + 1
,其中 n
用于存储队列元素,而另外 1 个位置用于约定为空。
初始化:
入队(Enqueue):
出队(Dequeue):
获取队头元素(Front):
判空(isEmpty):
判满(isFull):
获取队列大小(Size):
下面是基于数组的循环队列的简单实现:
class CircularQueue {
private int[] queue;
private int front; // 队头指针
private int rear; // 队尾指针
private int capacity; // 队列容量
public CircularQueue(int capacity) {
this.capacity = capacity + 1; // 约定一个空位置
queue = new int[this.capacity];
front = rear = 0;
}
public void enqueue(int element) {
if (isFull()) {
throw new RuntimeException("Queue is full");
}
queue[rear] = element;
rear = (rear + 1) % capacity;
}
public int dequeue() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
int element = queue[front];
front = (front + 1) % capacity;
return element;
}
public int front() {
if (isEmpty()) {
throw new RuntimeException("Queue is empty");
}
return queue[front];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear + 1) % capacity == front;
}
public int size() {
return (rear - front + capacity) % capacity;
}
}
这里的实现采用了取模运算来实现循环的效果,确保队尾指针在数组边界处正确回绕。
递归定义: 递归是指在函数的定义中使用函数自身的方法。递归可以将一个大型问题分解成一个或多个相似但规模较小的子问题。
递归调用的特点:
// 计算阶乘的递归函数示例
public class RecursiveExample {
public static int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
} else {
// 递归调用
return n * factorial(n - 1);
}
}
public static void main(String[] args) {
int result = factorial(5);
System.out.println("Factorial of 5: " + result);
}
}
期末卷综合题中考察了递归函数运行时其栈的调用情况(要画图),建议自己多写几个程序画出其栈的调用情况来练习。
调用栈(Call Stack): 在进行递归调用时,系统会为每次调用生成一个栈帧,这些栈帧按照调用的先后顺序形成一个调用栈。
栈帧的生成与销毁:
栈的特性:
堆栈的大小限制: 递归调用过深可能导致调用栈溢出。在实际应用中,可以通过调整堆栈大小或使用迭代的方式来避免栈溢出的问题。
理解递归调用和栈的关系有助于更好地理解递归的执行流程和调试递归函数。
期末卷在填空题中有考察中缀表达式和后缀表达式的转换。
函数调用栈: 编程语言中的函数调用过程通常使用栈来管理函数调用和返回。每次函数调用都会生成一个栈帧,将函数的局部变量、返回地址等信息保存在栈上。
表达式求值: 栈可以用于中缀表达式到后缀表达式的转换,以及后缀表达式的求值。这种应用在计算机编译器和计算器中常见。
以中缀表达式 `3 + 5 * (4 - 2)` 为例,演示中缀表达式转后缀表达式的顺序流程:
1. **中缀表达式:** `3 + 5 * (4 - 2)`
2. **初始化:** 创建一个空的后缀表达式字符串(用 `postfixExpression` 表示)和一个空的操作符栈(用 `operatorStack` 表示)。
3. **遍历中缀表达式:**
- 当遇到数字或字母时,直接输出到后缀表达式。当前缀表达式:`3`
- 当遇到操作符 `+` 时,由于栈为空,直接入操作符栈。当前操作符栈:`+`,当前后缀表达式:`3`
- 当遇到数字 `5` 时,直接输出到后缀表达式。当前操作符栈:`+`,当前后缀表达式:`3 5`
- 当遇到操作符 `*` 时,由于栈为空,直接入操作符栈。当前操作符栈:`+*`,当前后缀表达式:`3 5`
- 当遇到左括号 `(` 时,直接入操作符栈。当前操作符栈:`+*(`,当前后缀表达式:`3 5`
- 当遇到数字 `4` 时,直接输出到后缀表达式。当前操作符栈:`+*(`,当前后缀表达式:`3 5 4`
- 当遇到操作符 `-` 时,由于栈顶的操作符优先级低于当前操作符,直接入操作符栈。当前操作符栈:`+*(-`,当前后缀表达式:`3 5 4`
- 当遇到数字 `2` 时,直接输出到后缀表达式。当前操作符栈:`+*(-`,当前后缀表达式:`3 5 4 2`
- 当遇到右括号 `)` 时,弹出操作符栈中的所有操作符,直到遇到左括号 `(`,并输出到后缀表达式。当前操作符栈:`+*`,当前后缀表达式:`3 5 4 2 -`
- 当遇到右括号 `)` 时,弹出操作符栈中的所有操作符,直到遇到左括号 `(`,并输出到后缀表达式。当前操作符栈:`+`,当前后缀表达式:`3 5 4 2 - * +`
4. **遍历结束:** 遍历完中缀表达式后,弹出操作符栈中的所有操作符,并输出到后缀表达式。最终后缀表达式为:`3 5 4 2 - * +`
通过以上步骤,我们成功地将中缀表达式 `3 + 5 * (4 - 2)` 转换为后缀表达式 `3 5 4 2 - * +`。
括号匹配: 栈常用于检查表达式中的括号是否匹配。通过维护一个栈,可以在遍历表达式时判断括号的匹配情况。
浏览器前进和后退: 浏览器的历史记录可以使用两个栈来实现,一个用于记录前进的页面,一个用于记录后退的页面。
撤销机制: 在图形设计和文本编辑软件中,撤销操作可以使用栈来实现。每次操作都将状态压入栈中,撤销时弹出栈顶状态。
任务调度: 操作系统中的进程调度和任务调度通常使用队列来管理待执行的任务。先进先出的特性使得任务按照顺序执行。
打印队列: 打印任务通常会进入一个打印队列,按照先进先出的原则进行打印。
广度优先搜索(BFS): 图的广度优先搜索算法常常使用队列来管理待访问的节点,确保按层次遍历图。
消息传递: 在计算机通信中,消息传递的队列模型常用于实现异步通信。消息发送者将消息放入队列,接收者从队列中取出消息进行处理。
缓冲区: 缓冲区是队列的一种应用,用于平衡生产者和消费者之间的速度差异。例如,生产者产生数据,将其放入缓冲区,消费者从缓冲区取出数据进行处理。