这个月计划把《数据结构与算法分析-C语言描述》重温一遍,恶补一下自己数据结构与算法方面的短板。这几天断断续续把最基本的线性结构一章看完,主要是讲了表、栈和队列三种数据结构的原理、实现以及应用。
表主要操作有Insert、Delete和Find几种。数组实现能够在O(1)时间进行查找第K项操作,但是Insert和Delete操作则需要O(N)时间,因为其需要大量移动和复制数组元素;而链表实现则刚好相反,能够在O(1)时间内进行Insert和Delete的操作,但是其查找第K项的操作则需要O(N)时间。
栈是一种LIFO(Last-In-First-Out)的数据结构,主要操作有Push和Pop两种,Push操作把新元素压入栈顶,而Pop操作则把栈顶元素移出。栈的实现方法也有数组和链表两种,数组实现的主要缺点在于要先定义一个固定大小的数组,当保存的元素数量大于数组大小后,数据需要重新分配更大的空间并把原有元素复制到新空间中。栈结构主要的应用包括前缀、中缀、后缀表达式的计算与转换,以及程序运行时的函数调用也用到了栈结构。
队列是一种FIFO(First-In-First-Out)的数据结构,主要操作有Enqueue(入队)和Dequeue(出队)两种,和栈一样,队列也可通过数组或链表实现。队列结构的主要应用有很多,如计算机系统内的消息队列等。
1. Reversing Linked List
Given a constant K and a singly linked list L, you are supposed to reverse the links of every K elements on L. For example, given L being 1→2→3→4→5→6, if K = 3, then you must output 3→2→1→6→5→4; if K = 4, you must output 4→3→2→1→5→6.
Input Specification:
Each input file contains one test case. For each case, the first line contains the address of the first node, a positive N (<= 105) which is the total number of nodes, and a positive K (<=N) which is the length of the sublist to be reversed. The address of a node is a 5-digit nonnegative integer, and NULL is represented by -1.
Then N lines follow, each describes a node in the format:
Address Data Next
where Address is the position of the node, Data is an integer, and Next is the position of the next node.
Output Specification:
For each case, output the resulting ordered linked list. Each node occupies a line, and is printed in the same format as in the input.
Sample Input:
00100 6 4
00000 4 99999
00100 1 12309
68237 6 -1
33218 3 00000
99999 5 68237
12309 2 33218
Sample Output:
00000 4 33218
33218 3 12309
12309 2 00100
00100 1 99999
99999 5 68237
68237 6 -1
思路:既然题目已经给出了元素的个数,所以我使用了数组实现,毕竟数组的元素交换比链表的实现更简单。思路非常简单,读入数据,构造链表,然后交换链表元素并更新每个元素的Next指针。一开始中了一个坑就是,并不是所有元素都是链表中的元素,有些输入元素是没用的。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义的数组元素
typedef struct Node {
char s[6];
int data;
char next[6];
} Node;
// 交换数组中两个元素
void swap (Node *Array, int index1, int index2) {
Node temp;
if (index1 == index2) {
return ;
}
memcpy(&temp, Array + index1, sizeof(Node));
memcpy(Array + index1, Array + index2, sizeof(Node));
memcpy(Array + index2, &temp, sizeof(Node));
}
int main () {
int num;
int changeNum;
char baseAd[6];
Node *Array = NULL;
int i, j;
int mid;
scanf("%s %d %d", baseAd, &num, &changeNum);
if (0 == strcmp("-1", baseAd)) {
return 1;
}
Array = malloc (num * sizeof(Node));
if (NULL == Array) {
printf("Malloc Failed!\n");
return 1;
}
// 读入所有元素
for (i = 0; i < num; i++) {
scanf("%s %d %s", Array[i].s, &(Array[i].data), Array[i].next);
}
// 找到链表的头结点并放置在数组的0位置
for (i = 0; i < num; i++) {
if (0 == strcmp(Array[i].s, baseAd)) {
swap(Array, 0, i);
break;
}
}
// 从数组0位置的元素开始,根据元素的Next指针不断找到下一个位置应放置的元素,读到Next为-1则表示结束,并记录数组中有效元素的个数
for (i = 0; i < num - 1; i++) {
for (j = i + 1; j < num; j++) {
if (0 == strcmp(Array[i].next, Array[j].s)) {
swap(Array, i + 1, j);
break;
}
}
if (0 == strcmp(Array[i + 1].next, "-1")) {
num = i + 2;
break;
}
}
// reverse数组元素
mid = changeNum / 2;
for (i = 0; i < num; i += changeNum) {
if ((i + changeNum) > num) { //数组最后剩下的不足changeNum长的不需要reverse
break;
}
for (j = 0; j < mid; j++) {
swap(Array, i + j, i + changeNum - j - 1);
}
}
for (i = 0; i < num; i++) {
if (i != (num -1)) { // 更新元素的Next指针
strcpy(Array[i].next, Array[i + 1].s);
} else {
strcpy(Array[i].next, "-1");
}
printf("%s %d %s", Array[i].s, Array[i].data, Array[i].next); // 打印元素
if (i != (num - 1)) {
printf("\n");
}
}
free(Array); // 最后不能忘了释放空间
return 0;
}
2. 一元多项式求导
设计函数求一元多项式的导数。(注:xn(n为整数)的一阶导数为n*xn-1。)
输入格式:以指数递降方式输入多项式非零项系数和指数(绝对值均为不超过1000的整数)。数字间以空格分隔。
输出格式:以与输入相同的格式输出导数多项式非零项的系数和指数。数字间以空格分隔,但结尾不能有多余空格。注意“零多项式”的指数和系数都是0,但是表示为“0 0”。
输入样例:
3 4 -5 2 6 1 -2 0
输出样例:
12 3 -10 1 6 0
思路:这道题比较简单,实际上并不需要使用链表就可以输出,不过本着多写多练的心,还是用链表完整的写了一下。要注意的是,当导数为零的时候要输出“0 0”.
代码如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int co;
int power;
struct Node *next;
} Node;
int main () {
int co;
int power;
int flag = 0;
Node *head, *newNode, *tempNode;
head = malloc (sizeof (Node));
if (NULL == head) {
return 1;
}
head->co = 0;
head->power = 0;
head->next = NULL;
tempNode = head;
// scanf input
while (EOF != (scanf("%d %d", &co, &power))) {
newNode = malloc (sizeof (Node));
if (NULL == newNode) {
return 1;
}
newNode->co = co;
newNode->power = power;
newNode->next = NULL;
tempNode->next = newNode;
tempNode = newNode;
}
// cal result
tempNode = head->next;
if (NULL != tempNode) {
tempNode->co = tempNode->co * tempNode->power;
tempNode->power--;
if (0 != tempNode->co) {
flag = 1; // 导数不为0
printf("%d %d", tempNode->co, tempNode->power);
}
tempNode = tempNode->next;
while (NULL != tempNode) {
tempNode->co = tempNode->co * tempNode->power;
tempNode->power--;
if (0 != tempNode->co) {
printf(" %d %d", tempNode->co, tempNode->power);
}
tempNode = tempNode->next;
}
}
// 如果导数为0,则输出0 0
if (!flag) {
printf("0 0");
}
// free memory
tempNode = head;
while (NULL != tempNode) {
head = tempNode->next;
free(tempNode);
tempNode = head;
}
return 0;
}
3. 求前缀表达式的值
算术表达式有前缀表示法、中缀表示法和后缀表示法等形式。前缀表达式指二元运算符位于两个运算数之前,例如2+3*(7-4)+8/4的前缀表达式是:+ + 2 * 3 – 7 4 / 8 4。请设计程序计算前缀表达式的结果值。
输入格式说明:
输入在一行内给出不超过30个字符的前缀表达式,只包含+、-、*、\以及运算数,不同对象(运算数、运算符号)之间以空格分隔。
输出格式说明:
输出前缀表达式的运算结果,精确到小数点后1位,或错误信息“ERROR”。
样例输入与输出
- + + 2 * 3 – 7 4 / 8 4 : 13.0
- / -25 + * – 2 3 4 / 8 4 : 12.5
- / 5 + * – 2 3 4 / 8 2 : ERROR
- +10.23 : 10.2
思路:关于前缀、中缀和后缀表达式等基本上是要用到栈结构的。观察前缀表达式,一个运算符之后如果是两个操作数的话那么可以进行计算,因此可以想到,当从左到右读入表达式时,当读入运算符时压入栈中,当读入操作数时,判断栈顶是否也是操作数,如果是操作数,则pop出该操作数作为operand1,读入的操作数作为operand2,继续pop出栈顶的操作符(pop出操作数之后的栈顶元素肯定是操作符),然后进行运算。得到结果后,继续判断栈顶是否还是操作数,重复同样运算,直到不是操作数后,将结果压入栈中,继续读入下一个数。算法思路倒是问题不到,但是在字符串的操作方面遇到了些问题,然后又没有C/C++的调试环境,最后还是用java写了,java对字符串的操作还是比较简单的,特别是处理字符串和数字之间的转换。
代码如下:
import java.text.DecimalFormat;
import java.util.LinkedList;
import java.util.Scanner;
public class Main {
public static void main (String[] args) {
DecimalFormat decimalFormat = new DecimalFormat(".#");
Scanner in = new Scanner(System.in);
String line = in.nextLine(); // 读入一行输入
String[] array = line.split(" "); // 将输入字符串split
LinkedList<String> stack = new LinkedList<String>();
String temp;
String op;
double operand1, operand2;
for (int i = 0; i < array.length; i++) {
temp = array[i];
if (!isOperator(temp)) {
operand2 = Double.parseDouble(temp);
while (!stack.isEmpty() && !isOperator(stack.peek())) { //只要栈顶是操作数,则进行运算
operand1 = Double.parseDouble(stack.pop());
op = stack.pop();
if (op.equals("/") && operand2 == 0) { // 除数为0,输出ERROR
System.out.print("ERROR");
System.exit(0);
} else {
operand2 = calculate(operand1, operand2, op);
}
}
stack.push(String.valueOf(operand2)); // 当栈顶不再操作数,将当前结果压入栈中
} else {
stack.push(temp); // 如果输入是操作符,则直接压栈
}
}
operand1 = Double.parseDouble(stack.pop());
System.out.print(decimalFormat.format(operand1));
}
// 计算结果
public static double calculate (double opr1, double opr2, String op) {
double result = 0;
char c = op.charAt(0);
switch (c) {
case '+': result = opr1 + opr2; break;
case '-': result = opr1 - opr2; break;
case '*': result = opr1 * opr2; break;
case '/': result = opr1 / opr2; break;
}
return result;
}
// 判断是不是操作符
public static boolean isOperator (String s) {
if (s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
return true;
}
return false;
}
}
4. Pop Sequence
Given a stack which can keep M numbers at most. Push N numbers in the order of 1, 2, 3, …, N and pop randomly. You are supposed to tell if a given sequence of numbers is a possible pop sequence of the stack. For example, if M is 5 and N is 7, we can obtain 1, 2, 3, 4, 5, 6, 7 from the stack, but not 3, 2, 1, 7, 5, 6, 4.
Input Specification:
Each input file contains one test case. For each case, the first line contains 3 numbers (all no more than 1000): M (the maximum capacity of the stack), N (the length of push sequence), and K (the number of pop sequences to be checked). Then K lines follow, each contains a pop sequence of N numbers. All the numbers in a line are separated by a space.
Output Specification:
For each pop sequence, print in one line “YES” if it is indeed a possible pop sequence of the stack, or “NO” if not.
Sample Input:
5 7 5
1 2 3 4 5 6 7
3 2 1 7 5 6 4
7 6 5 4 3 2 1
5 6 4 3 7 2 1
1 7 6 5 4 3 2
Sample Output:
YES
NO
NO
YES
NO
思路:模拟入栈出栈操作。数是由1开始从小到大入栈的,如果出栈序列中出现了某数字,那么比该数字小的数字肯定已经先压入栈中。因此,算法思路是:记录最上一次压栈的数字lastInt,每当读入一个出栈序列中的数字时,如果该数字等于栈顶元素,则pop栈顶元素并继续读入下一个出栈序列上的数字;如果该数字不等于栈顶元素,则从lastInt+1开始,不断压入数字,直到压入当前出栈数字,过程中如果栈元素超过最大容量,则表示该出栈序列不可能,否则继续压栈并pop当前出栈数字。当所有出栈数字读入完,判断栈是否为空,如果不为空则表示该出栈序列不可能。
代码如下:
#include <iostream>
#include <stack>
using namespace std;
// 处理pop序列的函数
bool isPopSeq (int array[], int n, int m) {
stack<int> stack;
int curInt;
int lastOut = 0; // 记录上一次入栈的数字
for (int i = 0; i < n; i++) {
curInt = array[i];
if (!stack.empty() && stack.top() == curInt) { // 如果当前数字等于栈顶数字,则栈顶出栈
stack.pop();
continue;
}
if (stack.empty() || stack.top() < curInt) { // 如果当前数字小于栈顶数字,则从上一次压栈的数字开始不断压栈
for (int j = lastOut + 1; j <= curInt; j++) {
stack.push(j);
if (stack.size() > m) { // 如果栈空间不够,表示该序列不可能
return false;
}
}
lastOut = stack.top(); // 更新lastOut为当前栈顶元素
stack.pop(); // 将当前栈顶元素pop
}
}
if (!stack.empty()) {
return false;
}
return true;
}
int main () {
int m, n, k;
cin >> m >> n >> k;
int *pop = new int[n];
for (int i = 0; i < k; i++) {
for (int j = 0; j < n; j++) {
cin >> pop[j];
}
if (isPopSeq(pop, n, m)) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
}
return 0;
}