基础数据结构——栈(一篇博客教你搞定)

概述

计算机科学中,stack是一种线性的数据结构,只能在其一端添加数据和移除数据。习惯来说,可以移动的一端称为栈顶,另外一端不能操作的数据称为栈底,就如同子弹弹夹或者生活中的书

可以理解为后进先出

链表实现栈

先定义一个接口类,里面提供栈里面所需要的操作,再听过implement接口,便于方法的封装管理

public interface Stack <E>{
    boolean push(E value);
    E pop();
    E peek();
    boolean isEmpty();
    boolean isFull();
}

关于泛型

在上述代码中,Stack 接口使用了泛型 <E>,其中 E 是一个类型参数,代表栈中元素的类型。泛型的作用如下:

  1. 类型安全:通过使用泛型,Stack 接口可以确保栈中存储的元素类型是相同的,从而避免在运行时出现类型不匹配的问题。

  2. 代码重用:使用泛型,Stack 接口可以用于任何类型的元素,而不需要为每种类型创建一个新的栈实现。这使得代码更加通用和可重用。

  3. 提高代码的可读性:泛型提供了更好的代码可读性,因为它们允许在代码中明确指定栈中元素的类型。

  4. 减少强制类型转换:使用泛型可以减少源代码中的强制类型转换,从而减少了出错的机会,并提高了代码的可读性。

  5. 泛型方法:如果 Stack 接口中包含方法,这些方法也可以使用泛型,使得方法能够处理多种类型的参数。

  6. 泛型擦除:Java编译器会在编译时将泛型类型参数替换为它们的边界(如果没有指定边界,则默认为 Object),这个过程称为泛型擦除。这保证了泛型代码可以与不使用泛型的旧代码兼容。

通过使用泛型,Stack 接口可以提供更安全、灵活和可维护的代码。

我们来看栈的相关代码

public class LinkListStack <E>implements Stack<E>,Iterable<E> {
    public LinkListStack(int capacity) {
        this.capacity = capacity;
    }

    private int capacity;
    private int size;
    private Node<E> head=new Node<E>(null,null);
    static class Node<E>{
        E value;
        Node next;

        public Node(E value, Node next) {
            this.value = value;
            this.next = next;
        }
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            Node<E> p=head.next;//设置栈的头结点
            @Override
            public boolean hasNext() {
                return p!=null;
            }

            @Override
            public E next() {
                E Val=p.value;
                p=p.next;
                return Val;
            }
        };
    }

    @Override
    public boolean push(E value) {
        if(isFull()){
            return false;
        }
        head.next=new Node<E>(value,head.next);
        size++;
        return true;
    }

    @Override
    public E pop() {
        if(isEmpty()){
            return null;
        }
        Node<E> first=head.next;
        head.next=first.next;
        return first.value;
    }

    @Override
    public E peek() {
        if (isEmpty()){
            return null;
        }
        return (E) head.next.value;
    }

    @Override
    public boolean isEmpty() {
        return head.next == null;
    }

    @Override
    public boolean isFull() {
        return size==capacity;
    }

    public static void main(String[] args) {
        LinkListStack<Integer> linkListStack=new LinkListStack<>(10);
        linkListStack.push(1);
        linkListStack.push(2);
        linkListStack.push(3);
        linkListStack.push(4);
        linkListStack.pop();
        linkListStack.push(5);
        linkListStack.push(6);
        for(Integer value:linkListStack){
            System.out.println(value);
        }

    }
}

和之前学习的几种数据结构一样,我们通过定义一个栈的类,在里面再通过static对于内部类node进行封装。

Node 类被设计为 LinkListStack 的一个组成部分,用于表示栈中的节点。由于 Node 类不需要访问 LinkListStack 的实例变量或方法,因此它可以被声明为 static,从而提高内存效率和编译时的优化。我们再来巩固一下static关键字的好处

  1. 内存效率static 内部类不会持有对外部类的引用,因此不会增加外部类实例的内存占用。

  2. 独立性Node 类可能被设计为可以独立于 LinkListStack 类使用,或者在其他上下文中重用。

  3. 访问控制static 内部类可以有不同的访问修饰符,例如 publicprivate,这可以限制外部类对内部类的访问。

  4. 避免不必要的依赖:如果 Node 类不需要访问 LinkListStack 类的实例变量或方法,那么将其声明为 static 可以避免这种不必要的依赖。

  5. 编译时的优化:由于 static 内部类不依赖于外部类的实例,编译器可以在不涉及外部类的情况下优化 static 内部类的代码。

@Override
    public boolean isEmpty() {
        return head.next == null;
    }

    @Override
    public boolean isFull() {
        return size==capacity;
    }

在上面这两个判断是否为空或者满的方法中,逻辑就是返回头结点的下一个是否为空和栈的元素个数是否和容量相同,不难理解

 

@Override
    public boolean push(E value) {
        if(isFull()){
            return false;
        }
        head.next=new Node<E>(value,head.next);
        size++;
        return true;
    }

再添加操作中,我们先判断栈是否满了,满了的话无法完成添加操作,没有满的话则通过创建该节点,将该节点指向原来头结点的指向,再将头结点指向该节点

@Override
    public E pop() {
        if(isEmpty()){
            return null;
        }
        Node<E> first=head.next;
        head.next=first.next;
        return first.value;
    }

    @Override
    public E peek() {
        if (isEmpty()){
            return null;
        }
        return (E) head.next.value;
    }

再来看移除和返回栈顶的操作,移除方法就是将头结点指向下下个节点,返回操作直接返回头结点指向的值就行。

我们再来看实现迭代器的遍历操作

 @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            Node<E> p=head.next;//设置栈的头结点
            @Override
            public boolean hasNext() {
                return p!=null;
            }

            @Override
            public E next() {
                E Val=p.value;
                p=p.next;
                return Val;
            }
        };
    }

再便利操作中,当查询到节点的next为空时,代表已经遍历完了栈中所有的元素,可以结束遍历

数组实现栈

我们设置一个top指针,指向下一个加入栈中的元素在数组中存储对应的位置

我们可以利用top指针完成isempty和isfull的判断,不难理解,如下

 @Override
    public boolean isEmpty() {
        return top == 0;
    }

    @Override
    public boolean isFull() {
        return top==array.length;
    }

我们来看其他方法

添加方法只需要再判断是否为空的基础上将传入的值赋值给top,同时top完成自增

移除方法则是将top--即可,这样子再下一次添加操作中会直接覆盖移除位置的值

返回栈顶的方法不难理解,整体代码如下

 @Override
    public boolean push(E value) {
        if(isFull()){
            return false;
        }
        array[top++] = value;
        return true;
    }

    @Override
    public E pop() {
        if(isEmpty()){
            return null;
        }
        E value = array[top-1];
        top--;
        return value;
    }

    @Override
    public E peek() {
        if(isEmpty()){
            return null;
        }
        return array[top-1];
    }

关于迭代器遍历的方法,当p=0的时候说明遍历结束,然后按照返回值和完成p的递减即可

@Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            int p=top;
            @Override
            public boolean hasNext() {
                return p>0;
            }

            @Override
            public E next() {
                E value=array[p-1];
                p--;
                return value;
            }
        };
    }

应用一:有效的括号

我们来看一道题目木

我们来分析一下思路,我们可以将括号分为左括号和有括号部分,如果是左括号就将对应的右括号存入栈里面,再输入式子中的左括号全部处理完成之后再将依次读取的右括号与从栈顶拿出来的右括号判断是否相等,然后移除栈顶的右括号再次进行读取判断,如果最后栈里面还有元素,说明括号不正确。

注意,如果式子是以右括号开头,依然是不正确的,需要补充判断

我们来看代码

public class E01Leetcode20 {
    public boolean isVaild(String s) {
        ArrayStack<Character> stack = new ArrayStack<>(s.length());
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(') {
                stack.push(')');
            } else if (c == '[') {
                stack.push(']');
            } else if (c == '{') {
                stack.push('}');
            } else {
                if (stack!=null &&c == stack.peek()) {
                    stack.pop();
                } else {
                    return false;
                }
            }
        }
        return stack.isEmpty();
    }
}

 后缀表达式求值

思路如下:

遇到数字压入栈

遇到运算符,就从栈弹出两个数字做运算,将结果压入栈

遍历结束,栈中剩下的数字就是结果

将栈顶的值返回即为结果

我们来看代码

public class E02Leetcode150 {
    public int evalRPN(String[] tokens) {
        LinkedList<Integer> stack = new LinkedList<>();
        for (String t : tokens) {
            switch (t) {
                case "+"->{
                    Integer b=stack.pop();
                    Integer a=stack.pop();
                    stack.push(a+b);
                }
                case "-"->{
                    Integer b=stack.pop();
                    Integer a=stack.pop();
                    stack.push(a-b);
                }
                case "*"->{
                    Integer b=stack.pop();
                    Integer a=stack.pop();
                    stack.push(a*b);
                }
                case "/"->{
                    Integer b=stack.pop();
                    Integer a=stack.pop();
                    stack.push(a/b);
                }
                default -> {//数字
                    stack.push(Integer.parseInt(t));
                }
            }
        }
        return stack.pop();
    }
}

将中缀表达式转换为后缀表达式

类似于实现这样的效果,思路如下,先通过StringBuilder创建一个存储字符串的对象,在用过链表Linkedlist来实现栈,栈里面用来存储运算符

1,遇到非运算符,直接拼串

2,遇到+ - * /

      --它的优先级比栈顶运算符高,入栈

      --否则把栈里面优先级>=它的都出栈,它再入栈

3,遍历完成,栈里剩余运算符依次出栈

4,带()

      --左括号直接入栈,左括号先设置为0

      --右括号就把栈里到左括号为止的所有运算符出栈

代码如下

public class E03InfixToSuffix {
    public static void main(String[] args) {

    }
    static int priority(char c){
        return switch (c){
            case '+','-' -> 1;
            case '*','/' -> 2;
            case '(' -> 0;
            default -> throw new IllegalArgumentException("不合法的运算符"+c);
        };
    }
    static String infixToSuffix(String exp){
        LinkedList<Character> stack = new LinkedList<>();
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<exp.length(); i++){
            char c=exp.charAt(i);
            switch (c){
                case'*','/','+','-'->{
                    if(stack.isEmpty()){
                        stack.push(c);
                    }else {
                        if(priority(c)>priority(stack.peek())){
                            stack.push(c);
                        }else {
                            while(!stack.isEmpty() && priority(stack.peek())>=priority(c)){
                                sb.append(stack.pop());
                            }
                            stack.push(c);
                        }
                    }
                }
                case'('->{
                    stack.push(c);
                }
                case')'->{
                    while (!stack.isEmpty()&&stack.peek()!='('){
                        sb.append(stack.pop());
                    }
                    stack.pop();
                }
                default -> {
                    sb.append(c);
                }
            }
        }
        while(!stack.isEmpty()){
            sb.append(stack.pop());
        }
        return sb.toString();
    }
}

### RT-DETRv3 网络结构分析 RT-DETRv3 是一种基于 Transformer 的实时端到端目标检测算法,其核心在于通过引入分层密集正监督方法以及一系列创新性的训练策略,解决了传统 DETR 模型收敛慢和解码器训练不足的问题。以下是 RT-DETRv3 的主要网络结构特点: #### 1. **基于 CNN 的辅助分支** 为了增强编码器的特征表示能力,RT-DETRv3 引入了一个基于卷积神经网络 (CNN) 的辅助分支[^3]。这一分支提供了密集的监督信号,能够与原始解码器协同工作,从而提升整体性能。 ```python class AuxiliaryBranch(nn.Module): def __init__(self, in_channels, out_channels): super(AuxiliaryBranch, self).__init__() self.conv = nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1) self.bn = nn.BatchNorm2d(out_channels) def forward(self, x): return F.relu(self.bn(self.conv(x))) ``` 此部分的设计灵感来源于传统的 CNN 架构,例如 YOLO 系列中的 CSPNet 和 PAN 结构[^2],这些技术被用来优化特征提取效率并减少计算开销。 --- #### 2. **自注意力扰动学习策略** 为解决解码器训练不足的问题,RT-DETRv3 提出了一种名为 *self-att 扰动* 的新学习策略。这种策略通过对多个查询组中阳性样本的标签分配进行多样化处理,有效增加了阳例的数量,进而提高了模型的学习能力和泛化性能。 具体实现方式是在训练过程中动态调整注意力权重分布,确保更多的高质量查询可以与真实标注 (Ground Truth) 进行匹配。 --- #### 3. **共享权重解编码器分支** 除了上述改进外,RT-DETRv3 还引入了一个共享权重的解编码器分支,专门用于提供密集的正向监督信号。这一设计不仅简化了模型架构,还显著降低了参数量和推理时间,使其更适合实时应用需求。 ```python class SharedDecoderEncoder(nn.Module): def __init__(self, d_model, nhead, num_layers): super(SharedDecoderEncoder, self).__init__() decoder_layer = nn.TransformerDecoderLayer(d_model=d_model, nhead=nhead) self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers) def forward(self, tgt, memory): return self.decoder(tgt=tgt, memory=memory) ``` 通过这种方式,RT-DETRv3 实现了高效的目标检测流程,在保持高精度的同时大幅缩短了推理延迟。 --- #### 4. **与其他模型的关系** 值得一提的是,RT-DETRv3 并未完全抛弃经典的 CNN 技术,而是将其与 Transformer 结合起来形成混合架构[^4]。例如,它采用了 YOLO 系列中的 RepNCSP 模块替代冗余的多尺度自注意力层,从而减少了不必要的计算负担。 此外,RT-DETRv3 还借鉴了 DETR 的一对一匹配策略,并在此基础上进行了优化,进一步提升了小目标检测的能力。 --- ### 总结 综上所述,RT-DETRv3 的网络结构主要包括以下几个关键组件:基于 CNN 的辅助分支、自注意力扰动学习策略、共享权重解编码器分支以及混合编码器设计。这些技术创新共同推动了实时目标检测领域的发展,使其在复杂场景下的表现更加出色。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值