引言
学习了JAVA的基础知识以后,这是本人第一次接触数据结构与算法的知识,为了更高效以及自律的学习,我加入了Datawhale学习小组。
Task03------栈与递归
理论部分:
一、用数组实现一个顺序栈
1.栈的特征
和之前所讲的 数组、链表 等数据储存结构不同,“栈”更偏向于是一个“工具”而非一种储存结构,”先进后出,后进先出“ 以及 “受限访问” 是栈的基本特性,前者很好理解,后者意味着用户每次只能对栈顶元素进行访问和操作。基本操作有:
- 入栈 push()
- 出栈 pop()
- 查看栈顶元素 peek()
- 是否为空栈 isEmpty()
- 清空栈 clear()
2.顺序栈的特点
- 由数组实现的顺序栈是有容量限制的(即数组大小)。
- 由数组实现的顺序栈有一个 top指针 指向栈顶元素。
- 对栈顶数据元素执行“删除”后,实际上数据并未删除,只是将 top指针指向下一个元素,使之无法访问,直到数据被下一次覆盖。
3.代码实现
class SeqStack<T>{
private int maxSize;
private T[] stackArray;
private top;
SeqStack(int maxSize){
this.maxSize=maxSize;
stackArray=(T[])new Object[maxSize];
top=-1; //还没有元素
}
SeqStack(){
this(10); //默认大小为10
}
public void push(T data){
if(top==maxSize)
System.out.println("栈已满!");
else
stackArray[++top]=data;
}
public void pop(){
if(top==-1)
System.out.println("栈已空!");
else
top--;
}
public T peek(){
return stackArray[top];
}
public boolean isEmpty(){
return top==-1;
}
public void clear(){
top=-1;
}
}
二、用链表实现一个链栈
1.链栈的特点
- 由于是用链表实现,理论上链栈的容量是无限制的。
- 链表的 first结点 即为栈顶元素。
- 每次 入栈 应该 addFirst 这样才能实现 “先入后出,后入先出”。
- 每次的”删除“操作确确实实是吧栈顶元素删除了。
2.代码实现
此处采用 SLinkedList单向链表类 实现(该类上一篇blog已经实现,这里就省略了)。
class LinkedStack<T>{
private SLinkedList<T> list;
LinkedStack(){
list=new SLinkedList();
}
public void push(T data){
list.addFirst(data);
}
public void pop(){
list.remove(0);
}
public T peek(){
return list.get(0).getData();
}
public boolean isEmpty{
return list.isEmpty();
}
}
三、理解递归的原理
1.递归的条件
首先,“递归” 可以简单的理解为 “一个方法直接或间接调用方法自身”,但是这样的调用是有条件的:
- 在每一次调用自己时,必须是(在某种意义上)更接近于解。
- 必须有一个终止处理或计算的准则,我们称之为 “递归基” 或 “基例”
2.简单的递归问题
i>三角数字
数字序列 1,3,6,10,15,21, …可以被形象的表示为三角形排列,请编写程序查找第n项
public static void triangle(int n){
if(n==1)
return 1;
else
return( n+triangle(n-1) );
}
我们可以这样考虑三角数字的递归:
Amy现在想知道第n项的值,她只需要知道第n-1项的值然后加上n即可,于是她去问Bob第n-1项是多少。
Bob现在想知道第n-1项的值,于是他去问Carry第n-2项是多少。
Carry现在想知道第n-2项的值,于是。。。
最后一名同学已经知道第1项的值为1,于是他告诉了前一名同学,前一名同学告诉了再前一名同学。。。
最后Amy获得了n-1项的值,第n项的值也就呼之欲出。
ii>递归的二分查找
有了上一道题对递归的认识,现在我们将熟悉的二分查找用递归实现 (默认数组升序)
/**
*递归实现二分查找,返回元素下标
*@param arr: 所要查找的数组
*@param key: 要查找的元素
*@param low: 查找范围的下界
*@param high: 查找范围的上界
*/
public int search(int[] arr,int key,int low,int high){
int mid=(low+high)/2;
if(arr[mid]==key)
return mid;
else if(low>high)
return -1;
else{
if(arr[mid]<key)
return search(arr,key,mid+1,high);
else
return search(arr,key,low,mid-1);
}
}
同样的:
Amy想找到 key 的下标,但是她现在只知道 key>arr[mid] ,于是她去问Bob在 [mid+1,high] 上 key 的下标
Bob只知道 key > arr[(mid+1+high)/2],于是他去问Carry。。。
最后Amy就知道了key的下标
练习部分:
一、汉诺塔问题
1.要求
汉诺塔问题源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘,编写算法解出 n 个圆盘从 a -> c 的具体步骤。
2.分析
要实现 n 个圆盘 A >>> C ,必须完成以下三步:
- 将 n-1 个圆盘 A >>> B
- 将 1 个圆盘 A >>> C
- 将 n-1 个圆盘 B >>> C
那么,要将 n-1 个圆盘 A >>> B,则重复以上三步,直到最后只剩 1 个圆盘的移动
3.代码实现
import java.util.Scanner;
public class Hanoi {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
System.out.println("请输入圆盘个数:");
int n=in.nextInt();
hanoi(n,"A","B","C");
}
public static void hanoi(int n,String from,String mid,String to) {
if(n==1) {
System.out.println(from+"上圆盘"+" -> "+to);
return;
}
hanoi(n-1,from,to,mid); //第一步
System.out.println(from+"上圆盘"+" -> "+to); //第二步
hanoi(n-1,mid,from,to); //第三步
}
}
二、车辆重排问题
1.要求
假设一列货运列车共有n节车厢,每节车厢将停放在不同的车站。假定n个车站的编号分别为1至n,货运列车按照第n站至第1站的次序经过这些车站。车厢的编号与它们的目的地相同。为了便于从列车上卸掉相应的车厢,必须重新排列车厢,使各车厢从前至后按编号1至n的次序排列。当所有的车厢都按照这种次序排列时,在每个车站只需卸掉最后一节车厢即可。
我们在一个转轨站里完成车厢的重排工作,在转轨站中有一个入轨、一个出轨和k个缓冲铁轨(位于入轨和出轨之间)。图(a)给出一个转轨站,其中有k个(k=3)缓冲铁轨H1,H2 和H3。开始时,n节车厢的货车从入轨处进入转轨站,转轨结束时各车厢从右到左按照编号1至n的次序离开转轨站(通过出轨处)。在图(a)中,n=9,车厢从后至前的初始次序为5,8,1,7,4,2,9,6,3。图(b)给出了按所要求的次序重新排列后的结果。
编写算法实现火车车厢的重排,模拟具有n节车厢的火车和k个缓冲轨道的“入轨”和“出轨”过程。
2.分析
仔细思考一下这个题目,k个缓冲轨实际就是k个栈。因为车厢不能随意进出缓冲轨,所以一定要将所有的车厢按编号由小到大排列在缓冲轨上,然后按编号顺序依次出栈
3.代码实现
先上main方法,输入车厢数n和缓冲轨数k,其中有一个静态变量target,标记当前要出轨的是哪个编号的车厢
public class Text {
static int target=1;
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
System.out.print("请输入车厢节数n= ");
int n=in.nextInt();
System.out.print("请输入缓冲轨数量k= ");
int k=in.nextInt();
int[] cars=new int[n];
System.out.println("请输入每节车厢编号");
for(int i=0;i<n;i++)
cars[i]=in.nextInt();
while(!result(cars,k)) {
System.out.print("缓冲轨不够啦!请输入要添加的缓冲轨数量:");
int add=in.nextInt();
k+=add;
result(cars,k);
}
}
}
再上解决问题的主方法,设置为boolean类型是因为可能缓冲轨数量不足以实现重排。
(SeqStack是以数组实现的栈,此处省略其代码)
/**
*用于输出结果的方法
*@param cars: 车厢的编号
*@param k: 缓冲轨道数
*/
public static boolean result(int[] cars,int k) {
SeqStack<Integer>[] buffers=new SeqStack[k];
int current=0; //一个指针,用于判断车厢应该进入哪个缓冲轨
for(int i=0;i<k;i++) {
buffers[i]=new SeqStack<Integer>();
buffers[i].push(cars.length+1); //预先压入一个最大量,可以规避空栈的讨论
}
for(int j=cars.length-1;j>=0;j--) {
if(cars[j]==target) {
System.out.println(cars[j]+"号车厢:入轨 -> 出轨");
target++;
//每一次有车厢直接出轨的时候就要判断缓冲轨中是否有车厢可以出轨
while(output(buffers))
target++;
}
else {
for(current=0;current<k;current++)
if(buffers[current].peek()>cars[j])break; //使缓冲轨能够上小下大
if(current==k) //缓冲轨不够的情况
return false;
buffers[current].push(cars[j]);
System.out.println(cars[j]+"号车厢:入轨 -> 缓冲轨H"+(current+1));
}
}
return true;
}
接下来是让缓冲轨中车厢出轨的方法,基本思路为:
1.比较当前需要出轨的车厢号 target与k个栈顶中的最小编号。
2.如果相等则出栈,返回true,否则返回false。
public static boolean output(SeqStack<Integer>[] buffers) {
int min=buffers[0].peek(); //栈顶最小的编号
int min_position=0; //min车厢所在的缓冲轨
for(int i=1;i<buffers.length;i++)
if(buffers[i].peek()<min) {
min=buffers[i].peek();
min_position=i;
}
if(min==target) {
System.out.println(min+"号车厢:缓冲轨H"+(min_position+1)+" -> 出轨");
buffers[min_position].pop();
return true;
}
return false;
}