列表(List)
介绍
一系列有序元素的集合。列表中的元素可以是任意类型,允许重复。
- 可通过索引定位、访问列表中的(单个)元素,还可使用切片(slice)操作一次性访问多个元素,从而实现对列表的复杂操作。
- 通过对象引用来操作列表,例如:使用append()、insert()、remove()等方法实现动态地插入或删除元素(使用频繁)
了解:Java中提供了多种实现列表的类,如:
- ArrayList:基于动态数组实现的列表,支持快速随机访问和插入/删除操作(需要移动大量元素--在开头或中间插入/删除时,被插入/删除位置之后的所有元素都要往前/后移位)。
- LinkedList:基于链表实现的列表,支持快速插入/删除(较高效--只需要修改指针即可),不支持随机访问。
- Vector:与ArrayList类似,但是线程安全。
- Stack(先进后出(FILO)):基于Vector实现的栈。与LinkedList相比,Vector在访问元素时更高效--根据索引值直接获取元素,不需要遍历整个列表。
综上所述,ArrayList适合需要频繁随机访问元素,但不要求线程安全,并且数据量相对较小的场景。LinkedList适合频繁进行插入/删除操作而不需要随机访问元素,常出现在需要处理大量数据的场景。Vector适合需要频繁随机访问元素,并要求线程安全的同时具备动态扩容能力的场景,插入和删除操作少。Stack适合需要按照先进后出原则进行操作的场景,对于多线程并发场景应当使用线程安全的Stack实现类,如ConcurrentLinkedStack。
预备知识
1.包装类(Wrapper Class):
Java中的基本数据类型(int、double、boolean等)不能直接添加到列表中(列表只能包含对象类型),为了解决这个问题,Java提供了对应的包装类(Integer、Double、Boolean等),实现了将基本数据类型转化为对象类型,使得基本数据类型也能被添加至列表。
2.集合(Collection):
一种用于存储对象的容器,并提供一些常用的操作方法,如添加、删除、查找、遍历、排序等。其中,List接口用于存储有序元素的列表。
常用操作
1.初始化列表
/* 初始化列表 */
List<Integer> list1=new ArrayList<>();
Integer[] nums=new Integer[] {1,2,3,4,5};
List<Integer> list=new ArrayList<>(Arrays.asList(nums));
2.访问与更新元素
/* 访问与更新元素 */
int num=list.get(1);
list.set(1,0);
3.在列表中添加、删除元素
/* 在列表中添加、删除元素 */
list.clear();/* 清空列表 */
list.add(element);/* 向列表中添加单个个元素 */
list.add(index, element);/* 指定元素位置进行添加 */
/* 举例:list.add(3,6); //在第4个元素的位置插入6 */
list.remove(index);/* 删除指定位置 */
list.remove(element);/* 删除指定元素 */
/* 使用注意看看大佬这篇博客: https://blog.csdn.net/qq_36412715/article/details/84071160 */
4.遍历列表
/* 遍历列表 */
for(int n:list){
n
}
5.拼接两个列表
/* 拼接两个列表 */
List<Integer> list1=new ArrayList<>(Arrays.asList(new Integer[] {1,2,3,4,5}));
list2.addAll(list1);
6.排序列表
/* 排序列表 */
Collections.sort();/* 对列表进行升序排序 */
简易实现
/* 简易实现 */
class MyList{
private int[] nums;
private int capa=10;
private int size=0;
private int ratio=2;
public MyList(){}
size;
public nums[index]
nums[index]=s;
copyOf
}
public class MyArrayList<E> {
private Object[] elements; // 用数组实现列表
private int size; // 列表大小
// 构造方法
public MyArrayList(int initialCapacity) {
elements = new Object[initialCapacity];
size = 0;
}
// 默认构造方法
public MyArrayList() {
this(10);
}
// 添加元素到列表末尾
public void add(E e) {
if (size == elements.length) {
resize();
}
elements[size] = e;
size++;
}
// 在指定位置添加元素
public void add(int index, E e) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
if (size == elements.length) {
resize();
}
System.arraycopy(elements, index, elements, index + 1, size - index);
elements[index] = e;
size++;
}
// 获取指定位置的元素
@SuppressWarnings("unchecked")
public E get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
return (E) elements[index];
}
// 删除指定位置的元素
@SuppressWarnings("unchecked")
public E remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
E e = (E) elements[index];
System.arraycopy(elements, index + 1, elements, index, size - index - 1);
elements[size - 1] = null;
size--;
return e;
}
// 删除指定元素
public boolean remove(E e) {
for (int i = 0; i < size; i++) {
if (elements[i].equals(e)) {
remove(i);
return true;
}
}
return false;
}
// 修改指定位置的元素
public void set(int index, E e) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
elements[index] = e;
}
// 获取列表大小
public int size() {
return size;
}
// 判断列表是否为空
public boolean isEmpty() {
return size == 0;
}
// 判断列表中是否包含指定元素
public boolean contains(E e) {
for (int i = 0; i < size; i++) {
if (elements[i].equals(e)) {
return true;
}
}
return false;
}
// 清空列表
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
// 扩容
private void resize() {
Object[] newElements = new Object[elements.length * 2];
System.arraycopy(elements, 0, newElements, 0, size);
elements = newElements;
}
}
栈(Stack)
介绍
- 栈主要用于实现函数调用、表达式求值等需要倒序处理的场景。
- 具有先进后出(FILO)的特性,如下图解:
常用操作
方法 | 描述 | 时间复杂度 |
push() | 压入栈顶元素 | O(1) |
pop() | 栈顶元素出栈 | O(1) |
peek() | 访问栈顶元素 | O(1) |
/* 常用操作 */
Stack<Integer> stack=new Stack<>();
stack.push(1);
isEmpty();//判断栈是否为空;
栈的实现
基于链表的实现:
链表实现栈,主要需要考虑链表节点的插入和删除问题。
具体过程:每次插入新元素时,创建一个新节点并将其插入链表头部;每次弹出栈顶元素时,删除链表头部元素并返回其值即可。如下图解:
/* linkedlist_stack */
class LinkedListStack{
private ListNode stackPeek;
private int stkSize=0;
public void push(int num){
ListNode node=new ListNode(num);
node.next=stackPeek;
stackPeek=node;
stkSize++;
}
public void pop{
int num=peek();
stackPeek=stackPeek.next;
stkSize--;
return num;
}
}
public class LinkedListStack {
private Node top = null; // 栈顶指针
public void push(int value) {
Node newNode = new Node(value);
if (top == null) top = newNode; // 如果栈为空,将新节点作为栈顶指针
else {
newNode.next = top; // 将新节点放在栈顶,并更改栈顶指针
top = newNode;
}
}
public int pop() {
if (top == null) return -1; // 栈为空
int value = top.value;
top = top.next; // 修改栈顶指针
return value;
}
private static class Node {
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
}
}
基于数组的实现
数组实现栈,主要需要考虑如何实现栈的扩容问题。
具体过程是:当数组中元素个数已满时,开辟一个新的数组,并将原来数组中的元素全部复制到新数组中,然后再把新元素加入新数组。
数组栈的主要优势在于插入和删除都可以通过索引直接访问元素。
/* array_stack */
add()
return stack.remove(size()-1);
public class ArrayStack {
private int[] items; // 存放元素的数组
private int top = -1; // 栈顶指针
private int capacity; // 数组容量
public ArrayStack(int capacity) {
this.capacity = capacity;
items = new int[capacity];
}
public boolean push(int item) {
if (top == capacity - 1) return false; // 栈已满
items[++top] = item; // 先将栈指针+1,再赋值
return true;
}
public int pop() {
if (top == -1) return -1; // 栈为空
int item = items[top--]; // 先取值,再将栈指针-1
return item;
}
}
两种实现对比
支持操作:
时间效率:
都为O(1),推荐链表栈
空间效率:
数组栈需要预先分配一定大小的内存空间,会常用扩容操作。而链表栈不需要预先分配内存,通过动态增长、缩减空间,相比之下空间利用率更高。
分析总结:
使用数组作为底层实现可以快速插入和删除元素,但是需要考虑扩容问题;而使用链表作为底层实现则不需要考虑扩容问题,但是需要额外的空间存储指针信息。
在实际应用中,根据具体场景选择不同的底层实现方式能够更好地兼顾性能和空间效率。
典型应用
1.括号匹配
假设有一个字符串,其中包含小括号、中括号和大括号3种类型的括号,请编写一个函数判断该字符串中的括号是否匹配。
代码:
import java.util.Stack;
public class BracketMatch {
public static boolean isMatch(String s) {
// 使用 Stack<Character>类型的栈来存储遍历到的括号
Stack<Character> stack = new Stack<>();
// 当遍历到左括号时,将其压入栈中;
// 当遍历到右括号时,判断栈顶元素是否与之匹配,如果匹配则弹出栈顶元素,否则返回 false。
// 最后判断栈是否为空即可。
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') stack.push(c);
else if (c == ')' && !stack.isEmpty() && stack.peek() == '(') stack.pop();
else if (c == ']' && !stack.isEmpty() && stack.peek() == '[') stack.pop();
else if (c == '}' && !stack.isEmpty() && stack.peek() == '{') stack.pop();
else return false; // 遇到非法字符直接返回 false
}
return stack.isEmpty(); // 如果栈为空,则说明所有括号都匹配成功
}
public static void main(String[] args) {
String s1 = "()[]{}"; // true
String s2 = "([)]"; // false
String s3 = "({[]})"; // true
System.out.println(isMatch(s1));
System.out.println(isMatch(s2));
System.out.println(isMatch(s3));
}
}
2.表达式求值
假设有一个表达式字符串,其中包含加减乘除四种运算符和括号,请编写一个函数计算该表达式的结果。
代码:
import java.util.Stack;
public class ExpressionEvaluation {
public static int evaluate(String s) {
Stack<Integer> operands = new Stack<>(); // 存储操作数的栈
Stack<Character> operators = new Stack<>(); // 存储运算符的栈
for (char c : s.toCharArray()) {
if (c >= '0' && c <= '9') operands.push(c - '0'); // 如果是数字,则直接将其压入操作数栈中
else if (c == '+' || c == '-' || c == '*' || c == '/') { // 如果是运算符
while (!operators.isEmpty() && precedence(c) <= precedence(operators.peek())) { // 弹出优先级比当前运算符高或相等的运算符
char operator = operators.pop();
int b = operands.pop(), a = operands.pop(); // 取出栈顶的两个操作数
operands.push(calculate(a, b, operator)); // 计算结果并压回栈中
}
operators.push(c); // 将当前运算符入栈
}
else if (c == '(') operators.push(c); // 如果是左括号,则直接入栈
else if (c == ')') { // 如果是右括号,则弹出运算符并计算结果,直到遇到左括号
while (!operators.isEmpty() && operators.peek() != '(') {
char operator = operators.pop();
int b = operands.pop(), a = operands.pop();
operands.push(calculate(a, b, operator));
}
operators.pop(); // 弹出左括号
}
else throw new IllegalArgumentException("Invalid character: " + c); // 非法字符,抛出异常
}
while (!operators.isEmpty()) { // 计算剩余的运算符
char operator = operators.pop();
int b = operands.pop(), a = operands.pop();
operands.push(calculate(a, b, operator));
}
return operands.pop(); // 返回最后的结果,即为表达式的值
}
private static int precedence(char operator) { // 定义运算符的优先级
if (operator == '*' || operator == '/') return 2;
else if (operator == '+' || operator == '-') return 1;
else throw new IllegalArgumentException("Invalid operator: " + operator); // 非法运算符,抛出异常
}
private static int calculate(int a, int b, char operator) { // 计算两个操作数的结果
switch (operator) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return a / b;
default: throw new IllegalArgumentException("Invalid operator: " + operator); // 非法运算符,抛出异常
}
}
public static void main(String[] args) {
String s1 = "3+4*2/(1-5)+6/2"; // 7
String s2 = "2*(3+4)-5/2"; // 11
String s3 = "(1+2)*(3+4)"; // 21
System.out.println(evaluate(s1));
System.out.println(evaluate(s2));
System.out.println(evaluate(s3));
}
}
在上述代码中,我们使用了两个栈:一个存储操作数(整型数字),一个存储运算符。
遍历字符串时:
- 如果遇到数字则直接压入操作数栈中,
- 如果遇到运算符则将其与栈顶运算符比较优先级,如果当前运算符的优先级比栈顶高则入栈,否则弹出栈顶元素并计算结果。
- 遇到左括号则直接入栈,遇到右括号则取出运算符并计算结果,直到遇到左括号为止。
最后将剩余的操作数和运算符依次计算并返回即可。
即可。
队列(Queue)
介绍
- 用于存储按照时间顺序排列的元素。
- 队列具有“先进先出”(FIFO)的特性。如下图解:
常见操作
方法名 | 描述 | 时间复杂度 |
push() | 元素入队,即将元素添加至队尾 | O(1) |
poll() | 队首元素出队 | O(1) |
front() | 访问队首元素 | O(1) |
size() | 获取队列的长度 | O(1) |
isEmpty() | 判断队列是否为空 | O(1) |
队列实现
基于链表的实现
- 链表队列实现主要需要考虑节点的插入和删除问题。
- 具体过程:每次从队列中添加元素时,创建一个新的链表节点并将其插入到队列的尾部;每次删除元素时,删除链表头部的节点并返回其值。如下图解:
/* linkedlist_queue */
ListNoe front,rear;
queSize=0;
push(int num){
ListNode node=new ListNode(num);
if(front==null){
front=node;
rear=node;
}
else{
rear.next=node;
rear=node;
}
}
pop(){
int num=peek();
front=front.next;
queSize--;
return num;
}
public class LinkedListQueue {
private Node head = null; // 队首指针
private Node tail = null; // 队尾指针
public void enqueue(int value) {
Node newNode = new Node(value);
if (tail == null) { // 如果队列为空,头尾指针都指向新节点
head = newNode;
tail = newNode;
}
else {
tail.next = newNode; // 将新节点作为尾节点
tail = tail.next; // 修改尾节点指针
}
}
public int dequeue() {
if (head == null) return -1; // 队列为空
int value = head.value;
head = head.next; // 修改头节点指针
if (head == null) tail = null; // 如果队列中只有一个元素,则需要同时修改尾节点指针
return value;
}
private static class Node {
private int value;
private Node next;
public Node(int value) {
this.value = value;
}
}
}
基于数组的实现
- 数组队列实现需要注意当队列数组已满时,需要创建一个新的数组来扩充旧的数组。
- 具体过程:当队列数组已满时,让队列尾指针指向新的位置,重新开辟一个新的数组,将原队列中的数据复制到新队列中,并且释放原队列的空间。
/* array_queue */
class sss{
int[] num;
int front;
int queSize;
push(int num){
if
int rear=(front+queSize)%capa;
nums[rear]=num;
queSize++;
}
pop(){
peek;
front=(front+1)%capa;
queSize--;
}
};
public class ArrayQueue {
private int[] items; // 存放元素的数组
private int head = 0; // 队头指针
private int tail = 0; // 队尾指针
private int capacity; // 数组容量
public ArrayQueue(int capacity) {
this.capacity = capacity;
items = new int[capacity];
}
public boolean enqueue(int item) {
if (tail == capacity) return false; // 队列已满
items[tail++] = item;
return true;
}
public int dequeue() {
if (head == tail) return -1; // 队列为空
int item = items[head++];
return item;
}
}
两种实现对比
时间复杂度:
都为O(1)。
空间复杂度:
跟栈的说法相似。数组实现的队列需要预先分配一定大小的内存空间,在使用过程中如果超过了这个空间限制就需要进行扩容。而链表实现的队列不需要预先分配内存,可以动态增长或者缩小空间,所以空间利用率更高。
放松小时刻
关于栈和队列的一道例题:Problem - 1702
hdu 1702 “Acboy needs your help again!”
ACboy再次需要你的帮助!
时间限制:1000/1000 MS(Java/其他) 内存限制:32768/32768 K (Java/其他)
问题描述
阿童被绑架了!!
他非常想念他的母亲,现在非常害怕。你无法想象他被关进的房间有多暗,:(那么可怜。
作为一个聪明的ACMer,你想让ACboy走出怪物的迷宫。但当你到达迷宫的大门时,怪物说:“我听说你很聪明,但如果不能解决我的问题,你会和ACboy一起死去。
怪物的问题显示在墙上:
每个问题的第一行是一个整数N(命令的数量),和一个单词“FIFO”或“FILO”。(你很高兴,因为你知道“FIFO”代表“先进先出”,“FILO”的意思是“先进后出”)。
而接下来的N行,每行是“IN M”或“OUT”,(M代表一个整数)。
而问题的答案是一扇门的通行证,所以如果你想拯救ACboy,请仔细回答问题!
输入
输入包含多个测试用例。
第一行有一个整数,表示测试用例的数量。
每个子问题的输入如上所述。
输出
对于每个命令“OUT”,您应该输出一个整数,具体取决于单词“FIFO”或“FILO”,或者如果没有任何整数,则应输出单词“None”。
示例输入
4 4 FIFO IN 1 IN 2 OUT OUT 4 FILO IN 1 IN 2 OUT OUT 5 FIFO IN 1 IN 2 OUT OUT OUT 5 FILO IN 1 IN 2 OUT IN 3 OUT
示例输出
1 2 2 1 1 2 None 2 3
源
2007省赛集训队练习赛(1)
模拟栈和队列,栈是FILO,队列是FIFO。
--分析:分别用栈和队列模拟--
C++代码实现:
#include<bits/stdc++.h> //C++
using namespace std;
int main(){
int t,n,temp;
cin>>t;//测试的次数
while(t--){
string str,str1;
queue<int> Q;//定义一个队列
stack<int> S;//定义一个栈
cin>>n>>str;//输入每组要输入的个数,以及存储方式
for(int i=0;i<n;i++){
if(str=="FIFO"){//当为 FIFO时,表示队列
cin>>str1;//输入 IN或者是 OUT
if(str1=="IN"){
cin>>temp;
Q.push(temp);
}
else if(str1=="OUT"){
if(Q.empty()) cout<<"None"<<endl;//队列为空,输出 None
else{
cout<<Q.front()<<endl;//访问该队首
Q.pop();//删除队首元素
}
}
}
else{//当为 FILO,表示存储方式是栈
cin>>str1;
if(str1=="IN"){
cin>>temp;
S.push(temp);
}
else if(str1=="OUT"){
if(S.empty()) cout<<"None"<<endl;
else{
cout<<S.top()<<endl;
S.pop();
}
}
}
}
}
return 0;
}
//热爱C++的每一天