引入:
相比我们之前学习的数组,之前在一个数组中我们若是知道了一个数组中的某个数据的下标即可以立即的访问这个数据,
或者可以使用一个循环结构访问到每一项数据。
但是在本次学习的过程中,我们学习的这种数据结构,他的访问是受限制的,即在特定的时刻只有一个数据项可以被访问或者删除。
栈:
栈只允许访问一个数据项,即最后插入的数据项。只有移除这个数据项之后才能访问倒数第二个插入的数据项。后入先出
class StackX {
private int maxSize; // 栈数组的大小
private long[] stackArray;
private int top; // 栈顶
// 初始化栈信息 (构造器)
public StackX(int s) {
maxSize = s; // 初始化数组空间
stackArray = new long[maxSize]; // 创建数组
top = -1; // 初始化栈顶下标(表示当前没有数据)
}
/**
* 关于下面函数 ++top 的思考:
* 1. 因为顶的初始化值为-1, 若使用 top++ 的话,插入第一条数据的时候会报出空指针异常
* 2. 该数据结构的思想要求,栈顶的下标时刻指向最新插入的数据,若使用top++的话,执行完该句话以后,top就指向了下一个要插入数据的位置
*/
// 压入一条数据
public void push(long j) {
stackArray[++top] = j;
}
/**
* 结合上面的压入数据函数做一下思考:
* 1. 因为栈顶的指针时刻指向最新的数据所以在弹出的时候必须要使用 top 实时的值 不能发生变化
* 2. 在取了一条数据之后要求 top 自动指向前一条插入的数据,以方便下一次的取数据
* 3. 综上 在这里使用 top-- 是最合适的
*/
// 弹出一条数据
public long pop() {
return stackArray[top--];
}
// 查看栈顶元素
public long peek() {
return stackArray[top];
}
// 判断是否为空
public boolean isEmpty() {
return (top == -1);
}
// 判断是已经存满
public boolean isFull() {
return (top == maxSize-1);
}
}
public class Stack {
public static void main(String[] args) {
StackX theStack = new StackX(10);
theStack.push(20);
theStack.push(40);
theStack.push(60);
theStack.push(80);
while ( !theStack.isEmpty()) {
long value = theStack.pop();
System.out.print(value + " ");
}
System.out.println("");
}
}
····栈这种数据结构设计的很巧妙,在上面的代码中使用了数组的数据结构实现了栈:纵观全局使用了一个数组存储数据,操作一个 top 的数组下标来实现这种数据结构,个人觉得在用这种方式实现栈的核心思想有以下几点:
1. top 下标实时指向栈顶位置:这也是为什么 top 初值为 -1, 在压入的时候使用 ++top;在弹出的 时候使用 top–;
2. 他将一切操作数据的具体实现都用方法封装起来,就和上面引入中讲到的一样,在特定的情况下只有一条数据会被操作。
栈实例1 - 字符串反转:
输入一个字符串根据栈的性质:将该单词的字母依次压入栈中,然后在依次弹出,就会达到逆序的效果。
基于上面的代码:
class reverse {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入单词");
// 从控制台获取用户输入的单词
String word = sc.next();
int maxSize = word.length();
// 初始化栈信息
StackX2 theStack = new StackX2(maxSize);
// 将字符串分割为字符
char[] arr = word.toCharArray();
// 将分割好的字符依次压入栈中 (从第一个开始)
for (char c : arr) {
theStack.push(c);
}
// 再依次拿出
while ( !theStack.isEmpty() ) {
System.out.print(theStack.pop());
}
sc.close();
}
}
栈实例2 - 分隔符匹配:
输入一个带有分隔符的字符串 例如:a{b(c]d}e 判断他的左右分割符是否匹配:
需求解析:
先将字符串转换为字符数组;然后在将每个字符依次遍历出来,当判断到有左分隔符的时候将其压入栈中,当判断有右侧的分隔符时:将其赋值给一个变量,然后在从栈中弹出最新压入的字符,然后进行左右分割符是否匹配的判断,如果匹配那么就继续判断下一组,不匹配的话直接跳出总函数并且输出错误声明。
具体代码:
public void check() {
int stackSize = input.length(); // 获取到序判断的字符串的长度
StackX3 theStack = new StackX3(stackSize); // 初始化栈信息 实例一个对象出来
for (int j=0; j<stackSize; j++) { // 一次取出字符串中的每一个字符
char ch = input.charAt(j);
switch (ch) { // 判断分隔符类型
case '{': // 若为左分割符
case '[':
case '(':
theStack.push(ch); // 将分隔符压入栈中
break; // 终止当前的switch case 判断语句
case '}': // 若为右分隔符
case ']':
case ')':
if ( !theStack.isEmpty() ) { // 如果栈不为空(在遍历到右分割符的时候)
char chx = theStack.pop(); // 取出栈中的理论上与之对应的左分隔符
if ( (ch=='}' && chx!='{') || // 判断 :如果左右分隔符不匹配的话
(ch==']' && chx!='[') ||
(ch==')' && chx!='(') ) {
System.out.println("Error: " + ch + " at " + j); // 输出错误信息
}
} else { // 如果栈为空(在遍历到右分割符的时候) 直接输出错误信息
System.out.println("Error: " + ch + " at " + j);
}
break;
default:
break;
}
}
// 最后如果栈不为空 那么说明在字符串中只存在左分隔符 错误信息:丢失了右边的分隔符
if ( !theStack.isEmpty() ) {
System.out.println("Error: missing right delimiter");
}
}
栈的效率:
StackX中实现的栈,数据项入栈和出栈的时间复杂度都为常O(1)。这也就是说,栈操作所耗的时间不依赖于栈中的数据项的个数,因此操作时间很短。栈不需要比较和移动的操作。
队列:
队列作为一种数据结构,有点类似于栈。只不过栈是先进后出,而队列是先进先出(第一个插入的数据项会第一个被移除)。
1. 循环队列:
提出原因:假如我们向一个队列中插入数据,队列很快被装满了,我们取出一部分的数据之后,队列中空出来了位置让我们添加新的数据,但是队尾的指针不能在向后移动了,因此也无法添加新的数据了。根据此问题提出了循环队列来解决这个问题。
环绕式处理:
为了解决队列不满却不能继续插入数据的问题,可以让队头队尾指针绕回到数组开始的位置,这就是循环队列(有时也叫“缓冲环“);
演示代码:
public class Queue {
private int maxSize; // 队列的空间
private long[] queArray;
private int front; // 队头
private int rear; // 队尾
private int nItems; // 队列中的元素个数
// 初始化队列信息(构造器)
public Queue(int s) {
maxSize = s;
queArray = new long[maxSize];
front = 0;
rear = -1;
nItems = 0;
}
// 向队列中插入数据
public void insert(long j) {
if ( !this.isFull() ) {
if (rear == maxSize-1) { // 如果队列的队尾已经到了最顶端
rear = -1; // 则将其值重新赋为处置 (环绕式处理)
}
queArray[++rear] = j; // 这里使用的 rear++ 和栈中的原因是一样的
nItems++; // 元素个数增加
}
}
// 从队列中删除数据
public long remove() {
if ( !this.isEmpty() ) {
long temp = queArray[front++]; // 获取到将要删除的元素
if (front == maxSize) { // 如果目前的对头已经指向了最顶端
front = 0; // 则将其重新赋会初值 (环绕式处理)
}
nItems--;
return temp;
} else {
throw new NullPointerException();
}
}
// 获取当前对头指向的数据
public long peekFront() { return queArray[front]; }
// 判空
public boolean isEmpty() { return (nItems == 0); }
// 判满
public boolean isFull() { return (nItems == maxSize); }
// 获取当前队列的元素的个数
public int size() { return nItems; }
public static void main(String[] args) {
Queue theQueue = new Queue(5);
theQueue.insert(10);
theQueue.insert(20);
theQueue.insert(30);
theQueue.insert(40);
theQueue.remove();
theQueue.remove();
theQueue.remove();
theQueue.insert(50);
theQueue.insert(60);
theQueue.insert(70);
theQueue.insert(80);
while ( !theQueue.isEmpty()) {
long n = theQueue.remove();
System.out.println(n + " ");
}
}
}
在上面的代码中包含了数据项计数字段 nItems 会使 insert() 和 remove() 方法增加一点额外的操作,因为 insert() 和 remove() 方法必须分别递增和递减这个变量值。这可能算不上什么额外的开销,但是如果处理大量的插入和删除操作,这就可能会影响其性能了。
无数据项计数实现:
/**
* 不包含数据项计数字段 nItems
*/
public class Queue2 {
private int maxSize; // 队列容量
private long[] queArray;
private int front; // 队头
private int rear; // 队尾
public Queue2(int s) {
maxSize = s + 1;
queArray = new long[maxSize];
front = 0;
rear = -1;
}
public void insert(long j) {
if (!this.isFull()) {
if (rear == maxSize -1) {
rear = -1;
}
queArray[++rear] = j;
}
}
public long remove() {
if (!this.isEmpty()) {
long temp = queArray[front++];
if (front == maxSize) {
front = 0;
}
return temp;
} else {
throw new NullPointerException();
}
}
public long peek() { return queArray[front]; }
/**
* 解析:
* 1. 下面的判空 队尾+1 = 队头 或者 队头+容量-1 = 队尾
*/
public boolean isEmpty() { return (rear+1 == front || (front+maxSize-1 == rear)); }
/**
* 解析:
* 1. 下面的判满 队尾+2 = 队头 或者 队头+容量-2 = 队尾
*/
public boolean isFull() { return (rear+2 == front || (front+maxSize-2 == rear)); }
/**
* 解析:
* 1.
*/
public int size() {
if (rear >= front) {
return rear-front+1;
} else {
return (maxSize-front) + (rear+1);
}
}
}
但是由于上述方法中的 isEmpty() isFull() size() 方法的复杂程度 所以在实际情况中很少使用这种方式实现队列了
优先级队列
/**
* 优先级队列
*/
public class PriorityQ {
private int maxSize; // 队列容量
private long[] queArray;
private int nItems; // 数据计数项
// 初始化队列参数
public PriorityQ(int s) {
maxSize = s;
queArray = new long[maxSize];
nItems = 0;
}
// 插入操作(核心)
public void insert(long item) {
int j;
if (nItems==0) { // 如果插入之前为空队列
queArray[nItems++] = item; // 直接插入数据
} else { // 插入之前为非空队列
for (j=nItems-1; j>=0; j--) { // 依次拿出之前插入的数据(从后往前拿)
if (item > queArray[j]) { // 如果新添加的数据比之前的数据大
queArray[j+1] = queArray[j]; // 将原队列中的数据向后移一位
} else { // 如果新添加的数据比之前的数据小
break; // 跳出循环
}
}
queArray[j+1] = item; // 将数据添加进去
nItems ++;
}
}
// 取出数据
public long remove() { return queArray[--nItems]; }
// 取出队列中的最小值
public long peekMin() { return queArray[nItems-1]; }
// 判空
public boolean isEmpty() { return (nItems == 0); }
// 判满
public boolean isFull() {return (nItems == maxSize); }
// 测试
public static void main(String[] args) {
PriorityQ thePQ = new PriorityQ(5);
thePQ.insert(30);
thePQ.insert(50);
thePQ.insert(10);
thePQ.insert(40);
thePQ.insert(20);
while (!thePQ.isEmpty()) {
long item = thePQ.remove();
System.out.println(item + " ");
}
System.out.println("");
}
}
上述的优先级队列中,插入操作需要O(N)的时间,而删除操作则需要O(1)的时间,后续将会增加改进优先级队列插入操作的时间。
双端队列:
双端队列就是一个两段都是队尾的队列。队列的左右两段都可以进行插入和删除的操作。insertLeft、insertRight、removeLeft、removeRight 为实现其的关键方法。如果严格禁止调用insertLeft、removeLeft 方法,双端队列的功能就和栈一样。禁止调用 insertLeft、removeRight,它的功能就和队列一样。双端队列与栈或者队列相比,是一种多用途的数据结构,在容器类中有时会使用双端队列来提供栈和队列两种方法。
具体代码实现:
/**
* 双端队列的实现(包括环绕式处理)
*/
public class Queue_Practice02 {
private int maxSize;
private long[] queueArray;
private int rear_left;
private int rear_right;
private int front_left;
private int front_right;
private int nItems;
public Queue_Practice02(int s) {
maxSize = s;
queueArray = new long[maxSize];
rear_left = -1;
rear_right = maxSize;
front_left = 0;
front_right = maxSize-1;
nItems = 0;
}
public void insertLeft(long j) {
if (nItems == maxSize) {
rear_left = -1;
}
queueArray[++ rear_left] = j;
nItems ++ ;
}
public void insertRight(long j) {
if (nItems == maxSize) {
rear_right = maxSize;
}
queueArray[-- rear_right] = j;
nItems ++ ;
}
public long removeLeft() {
if (front_left == maxSize) {
front_left = 0;
}
long temp = queueArray[front_left];
queueArray[front_left] = 0;
front_left++;
nItems --;
return temp;
}
public long removeRight() {
if (front_right == 0) {
front_right = maxSize-1;
}
long temp = queueArray[front_right];
queueArray[front_right] = 0;
front_right --;
nItems --;
return temp;
}
public boolean isFull() {
return (nItems == maxSize);
}
public boolean isEmpty() {
return (nItems == 0);
}
// 测试
public static void main(String[] args) {
Queue_Practice02 theQueue = new Queue_Practice02(10);
theQueue.insertLeft(10);
theQueue.insertLeft(20);
theQueue.insertLeft(30);
theQueue.insertLeft(40);
theQueue.insertLeft(50);
theQueue.removeLeft();
theQueue.removeLeft();
theQueue.removeLeft();
theQueue.insertRight(10);
theQueue.insertRight(20);
theQueue.insertRight(30);
theQueue.insertRight(40);
theQueue.insertRight(50);
theQueue.removeRight();
theQueue.removeRight();
theQueue.removeRight();
while (!theQueue.isEmpty()) {
System.out.println(theQueue.removeLeft());
System.out.println(theQueue.removeRight());
}
}
}
注:双端队列看起来似乎比栈和队列更加灵活,但实际应用中远不及栈和队列有用。
作者还是小白一个: 第一次发表博客 还希望各位大神发现问题可以指点指点!!!
本文介绍了栈和队列这两种基本数据结构。栈遵循后入先出(LIFO)原则,常见应用如字符串反转;队列遵循先进先出(FIFO)原则,循环队列解决了队列满后无法继续插入的问题。此外,文章还提及了优先级队列和双端队列的概念,探讨了它们的特性和应用场景。
350

被折叠的 条评论
为什么被折叠?



