在之前基礎的介紹中,我有寫過一篇關於棧的內容(https://blog.csdn.net/weixin_45432742/article/details/99850913),介紹了棧以鏈表實現的方法和以以數組實現的方法,但是實現的棧將其存儲的數據類型限制了,只能存儲int類型的數據。在這裏,再優化一下,加入泛型,並用順序表實現一個棧結構,並且將其擴展,變成一個雙端棧結構。
棧
棧是限定僅在表尾進行插入和刪除操作的線性表,注意這裏說的是表尾喔,是拿線性表實現的,線性表的表尾是從數組的頭開始向後添加元素的。因此拿線性表去實現一個在棧頂操作的棧結構,在表尾進行操作比較方便(當然也可以從表尾向表頭方向進行操作,都是一樣的啦!)。下圖是棧的操作示意圖(再回顧一下下!)
其實棧本身也就是一個線性表,因爲它的數據元素也具有線性關係,因爲它的操作特殊,在棧頂進行操作,所以他是一種特殊的線性表。因爲後進先出的特點,棧又可以被稱爲後進先出的線性表。
雖然是拿線性表實現的,但是它是棧的操作,因此有一個棧的接口(因爲棧的操作受限,所以方法較List來說較少),如下類圖和實現:
package 棧;
/**
* Stack是棧的接口
*
* */
public interface Stack<E> {
/**
* 獲取棧中元素的個數
* @return 棧中的元素個數
* */
public int getSize();
/**
* 判斷當前棧是否爲空
* @return 是否爲空布爾值
* */
public boolean isEmpty();
/**
* 進棧一個元素e
* @param e 即將進棧的元素
* */
public void push(E e);
/**
* 出棧一個元素
* @return 當前出棧的棧頂元素
* */
public E pop();
/**
* 獲取當前棧頂元素
* @return 當前棧頂的元素 不彈棧
* */
public E peek();
/**
* 清空棧
* */
public void clear();
}
現在我們用我們寫好的線性表(參考上一篇https://blog.csdn.net/weixin_45432742/article/details/100702745)中的方法實現棧的操作,在Stack接口下有一個子類叫ArrayStack,它是用數組,也可以說是線性表實現的。現在我們來看一下它的類圖和具體實現:
package 棧;
import 線性表.ArrayList; //注意在Java內置中的util包下有ArrayList的實現子類,但是這裏要用的ArrayList類是自己寫的,因此在導包的時候要注意導包的位置。
public class ArrayStack<E> implements Stack<E> {
private ArrayList<E> list;
/**
* 創建默認大小的棧
*/
public ArrayStack() {
list = new ArrayList<E>();
}
/**
* 創建指定容量大小的棧
* @param capacity
*/
public ArrayStack(int capacity) {
list = new ArrayList<E>(capacity);
}
@Override
public int getSize() { //獲取棧當前存儲元素的個數
return list.getSize(); //這裏直接調用List裏面的方法,獲取當前有效的元素個數
}
@Override
public boolean isEmputy() { //判空
return list.isEmputy();
}
@Override
public void push(E e) { //壓棧,進棧是從尾部進棧
list.addLast(e); //因此調用List的尾部添加方法
}
@Override
public E pop() { //彈棧,刪除尾部的元素
return list.removeLast(); //調用的是List的從尾部刪除的方法
}
@Override
public E peek() { //獲取棧頂元素,就是獲取尾部元素,只訪問,不刪除
return list.getLast();
}
@Override
public void clear() { //清空棧,棧頂指針回到棧尾,而這裏是尾指針回到數組頭
list.clear();
}
@Override
public boolean equals(Object obj) { //這裏是棧的equals方法,這裏重寫的是object中的equals方法。比的是棧,但是這個順序棧是用順序表實現的
if(obj==null) {
return false;
}
if(obj==this) {
return true;
}
if(obj instanceof ArrayStack) {
ArrayStack stack = (ArrayStack) obj; //因此這裏要把傳進來的棧對象先強轉成內部棧對象
return list.equals(stack.list);//然後再用棧內當前的list對象與傳進來的棧對象調用的內部的list對象做比較,這樣比較的類型就是相同的了。
}
return false;
}
@Override
public String toString() { //tostring方法,這裏重寫的是object中的toString方法
StringBuilder sb = new StringBuilder();
sb.append("ArrayStack:size="+getSize()+",capaicty="+list.getCapacity()+"\n");
if(isEmputy()) {
sb.append("[]");
}else {
sb.append('[');
for(int i=0;i<getSize();i++) {
sb.append(list.get(i));
if(i==getSize()-1) {
sb.append(']');
}else {
sb.append(',');
}
}
}
return sb.toString();
}
}
雙端棧
雙端棧是指將一個線性表的兩端當做棧底進行入棧和出棧操作,也就是將普通只能在一端進行操作的棧,添加了一些功能,使之能在兩邊進行操作!,如下是它的結構圖:
那麼它的方法操作相對於普通棧來說就比較麻煩一些,因爲進棧的時候我們要判斷往那邊進,出棧從哪邊出,然後它的元素是不連續的,因此要獲得雙端棧的有效長度,要將兩邊的有效元素進行相加。那麼問題來了,我們如何定義進棧的方向呢?常規方法是規定兩個變量如0和1,如果傳進來的是0,默認左邊進/出棧;傳進來的是1,默認右邊進/出棧。但是這樣有一個不好的一點是,如果別人來看你寫的代碼,可讀的理解性不高,因此這裏給出定義枚舉變量的方法,定義兩個方位變量left和right,然後根據方位變量判斷時左邊還是右邊的操作,依賴於枚舉類。現在我們來看雙端棧的具體實現:
雙端棧也是棧的的一種,所以實現的也是棧接口。
package 棧;
import 線性表.ArrayList;
public class ArrayStackDoubleEnd<E> implements Stack<E>{
enum Direction{ //enum類定義了兩個方向變量
LEFT,RIGHT;
}
private E[] data; //用於存儲數據的容器
private int leftTop; //左端棧的棧頂 開始在-1
private int rightTop; //右端棧的棧頂 開始在data.length
private static int DEFAULT_SIZE=10; //默認容量大小爲10
/**
*定義默認容量的雙端棧
*/
public ArrayStackDoubleEnd(){
this(DEFAULT_SIZE);
}
/**
*定義指定容量的雙端棧
*/
public ArrayStackDoubleEnd(int capacity){
data=(E[]) new Object[capacity];
leftTop=-1;//左指針在數組的頭前
rightTop=data.length; //右指針在數組的末尾後
}
private boolean isFull(){ //判滿
return leftTop+1==rightTop; //當左右兩個指針相遇了就滿了
}
/**
* 向指定的端口進棧元素
* */
public void push(Direction dir,E e){
if(isFull()){ //進棧先判滿
resize(data.length*2); //滿了就擴容
}
if(dir==Direction.LEFT){ //未滿就進棧,判斷進棧的方向
data[++leftTop]=e; //添加在左邊棧頂
}else{
data[--rightTop]=e; //添加在右邊棧頂
}
}
private void resize(int newLen) { //擴容/縮容
E[] newData=(E[]) new Object[newLen]; //定義一個新的數組
//左端複製
for(int i=0;i<=leftTop;i++){
newData[i]=data[i];
}
//右端複製
int index=data.length-1;
int i;
//這裏爲什麼要用新數組的長度減去舊數組的長度再加右棧頂的位置呢?
//原因是這裏的i決定的是新數組插入元素的下標,
//所以用舊數組長度減去右棧頂的位置就是右邊棧的有效元素的個數,
//再用新數組的長度減去有效元素的個數就是擴容或者縮容後右棧頂的位置
for(i=newData.length-1;i>=newData.length-data.length+rightTop;i--){
newData[i]=data[index--];
}
rightTop=i+1;
data=newData; //不要忘記把舊數組替換成新數組喔
}
/**
* 從指定的端口出棧元素
* */
public E pop(Direction dir){ //使用枚舉類型傳入變量
if(dir==Direction.LEFT){ //如果爲左
if(leftTop==-1){ //出棧要判空
throw new IllegalArgumentException("左端棧爲空!");
}
E e=data[leftTop--]; //同樣要把出棧的元素先提取出來
if(getSize()<=data.length/4&&data.length>DEFAULT_SIZE){
resize(data.length/2); //元素太少,容量太大必要時記得縮容喔
}
return e; //將出棧的元素返回
}else{
if(rightTop==data.length){ //與左邊出棧方式一致
throw new IllegalArgumentException("右端棧爲空!");
}
E e=data[rightTop++];
if(getSize()<=data.length/4&&data.length>DEFAULT_SIZE){
resize(data.length/2);
}
return e;
}
}
/**
* 從指定的端口獲取棧頂元素
* */
public E peek(Direction dir){
if(dir==Direction.LEFT){ //獲取元素也要先判空
if(leftTop==-1){
throw new IllegalArgumentException("左端棧爲空!");
}
return data[leftTop]; //與出棧差別在於獲取的元素不用提取出來,元素也不移動,直接返回就行
}else{
if(rightTop==data.length){
throw new IllegalArgumentException("右端棧爲空!");
}
return data[rightTop];
}
}
/**
* 獲取指定端口棧的元素個數
* */
public int getSize(Direction dir){
if(dir==Direction.LEFT){
return leftTop+1;
}else{
return data.length-rightTop;
}
}
/**
* 判斷指定端口的棧是否爲空
* */
public boolean isEmpty(Direction dir){
if(dir==Direction.LEFT){
return leftTop==-1; //左端爲空在數組頭前
}else{
return rightTop==data.length; //右端爲空在數組末尾後
}
}
/**
* 清空指定端口的棧
* */
public void clear(Direction dir){
if(dir==Direction.LEFT){
leftTop=-1; //直接將指針移回初識狀態的位置
}else{
rightTop=data.length;
}
}
/**
* 獲取左端棧和右端棧元素的總和
* */
@Override
public int getSize() { //直接調用獲取指定位置元素的方法,並將兩個參數傳進去,再加起來
return getSize(Direction.LEFT)+getSize(Direction.RIGHT);
}
/**
* 判斷左端棧和右端棧是否全爲空
* */
@Override
public boolean isEmpty() { //判斷的是整個棧是否爲空
return isEmpty(Direction.LEFT)&&isEmpty(Direction.RIGHT);
}
/**
* 哪端少進哪端
* */
@Override
public void push(E e) { //不指定方向進棧一個元素
if(getSize(Direction.LEFT)<=getSize(Direction.RIGHT)){
push(Direction.LEFT,e);
}else{
push(Direction.RIGHT,e);
}
}
/**
* 哪端多彈哪端
* */
@Override
public E pop() { //不指定方向彈出一個元素
if(isEmpty()){
throw new IllegalArgumentException("雙端棧爲空!");
}
if(getSize(Direction.LEFT)>getSize(Direction.RIGHT)){
return pop(Direction.LEFT);
}else{
return pop(Direction.RIGHT);
}
}
/**
* 哪端多獲取哪 一樣多默認左
* */
@Override
public E peek() {
if(isEmpty()){
throw new IllegalArgumentException("雙端棧爲空!");
}
if(getSize(Direction.LEFT)>getSize(Direction.RIGHT)){
return peek(Direction.LEFT);
}else{
return peek(Direction.RIGHT);
}
}
/**
* 左右兩端都清空
* */
@Override
public void clear() { //調用代參的clear方法
clear(Direction.LEFT);
clear(Direction.RIGHT);
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("ArrayStackDoubleEnd:size="+getSize()+",capacity="+data.length+"\n");
if(isEmpty()){
sb.append("[]");
}else{
sb.append('[');
int count=0;
for(int i=0;i<=leftTop;i++){
sb.append(data[i]);
count++;
if(count==getSize()){
sb.append(']');
}else{
sb.append(',');
}
}
for(int i=rightTop;i<data.length;i++){
sb.append(data[i]);
count++;
if(count==getSize()){
sb.append(']');
}else{
sb.append(',');
}
}
}
return sb.toString();
}
/**
* 純粹比較兩個雙端棧的內容
* */
@Override
public boolean equals(Object obj) {
if(obj==null){ //如果傳進來的爲空則不相等
return false;
}
if(obj==this){ //傳進來的爲自己則肯定相等
return true;
}
if(obj instanceof ArrayStackDoubleEnd){ //如果傳進來的是雙端棧的對象
ArrayStackDoubleEnd<E> stack=(ArrayStackDoubleEnd) obj; //先要強轉一下
if(getSize()==stack.getSize()){ //然後判斷兩雙端棧總的有效元素是否相等
//相等的話要判斷兩棧的存儲的元素是否相同
//考慮到兩個棧對應的左右位置可能不相同,但是最後存儲的元素可能是相同的如棧1存儲的是12 3456,
//而棧2存儲的是1234 56,那麼用指針就不好判斷,所以先將兩個棧中的元素存儲到list順序表中,再去比較
ArrayList<E> list1=new ArrayList<E>(getSize());
ArrayList<E> list2=new ArrayList<E>(getSize());
//拼接當前棧的左部分
for(int i=0;i<=leftTop;i++){
list1.addLast(data[i]);
}
//拼接當前棧的右部分
for(int i=rightTop;i<data.length;i++){
list1.addLast(data[i]);
}
//拼接傳入棧的左部分
for(int i=0;i<=stack.leftTop;i++){
list2.addLast(stack.data[i]);
}
//拼接傳入棧的右部分
for(int i=stack.rightTop;i<stack.data.length;i++){
list2.addLast(stack.data[i]);
}
return list1.equals(list2); //最後調用list的equals方法去比較兩個順序表的內容
}
}
return false;
}
}
關於棧結構的內容就介紹完啦,看到這篇文章的小夥伴可以將代碼copy下來進行測試喔,有問題隨時評論區滴滴我!