蓝桥杯第十五届抱佛脚(二)内置数据结构
文章目录
- 蓝桥杯第十五届抱佛脚(二)内置数据结构
- 在竞赛中常见的数据结构
- 数组(Array)
- 链表(Linked List)
- 栈(Stack)
- 队列(Queue)
- 树(Tree)
- 映射(Map)
- 内置数据结构的快速使用
- 迭代器(Iterator)
- Vector 容器(类)
- 线性表基本使用
- 引入
- 创建List
- 添加元素
- 访问元素
- 修改元素
- 删除元素
- 遍历List
- List大小
- List实现类
- 队列 Queue
- 定义方式
- 基本使用
- Map 映射
- 定义方法
- 基本使用
- 成员方法
- Set 集合使用
- 定义方法
- 基本使用
- 自定义排序准则
- 使用Comparator进行自定义排序
- 例题练习
- Vector 的使用例题
- 队列使用例题
- Map使用例题
在竞赛中常见的数据结构
数组(Array)
-
数组是一种线性数据结构,在内存中是一块连续的存储空间。可以通过索引随机访问元素,查找效率很高。但数组大小固定,插入和删除操作效率低下。
int[] arr = new int[10]; // 声明一个长度为10的整型数组 arr[0] = 1; // 访问和赋值
-
数组支持随机访问,查找效率高,适合存储基础数据类型和需要频繁查询的数据。但插入和删除效率低。
例子:给定一个数组,求数组中是否有两个数之和等于目标值(两数之和)。我们可以先排序,然后使用双指针从首尾向中间扫描,时间复杂度O(nlogn)。
int[] twoSum(int[] nums, int target) { Arrays.sort(nums); int i = 0, j = nums.length - 1; while (i < j) { int sum = nums[i] + nums[j]; if (sum == target) return new int[]{i, j}; else if (sum < target) i++; else j--; } return new int[]{-1, -1}; }
链表(Linked List)
-
链表是一种动态数据结构,每个节点包含数据和指向下一个节点的引用。优点是内存动态分配,插入删除效率高,缺点是访问元素需要遍历,查找效率低。
// 单链表节点定义 class ListNode { int val; ListNode next; ListNode(int x) { val = x; } }
-
链表适合需要频繁插入和删除的场景。单链表无法高效查找和访问指定位置的元素,双向链表可以有效解决这个问题。
例子:反转一个单链表(链表反转)。我们可以使用迭代或递归的方式进行反转。
// 迭代反转单链表 ListNode reverseList(ListNode head) { ListNode prev = null, cur = head; while (cur != null) { ListNode next = cur.next; cur.next = prev; prev = cur; cur = next; } return prev; }
栈(Stack)
-
栈是一种先进后出(LIFO)的线性数据结构。Java中可以用数组或链表实现。
// 基于数组实现的栈 class ArrayStack { int[] arr = new int[100]; // 存储元素的数组 int top = -1; // 栈顶指针 void push(int x) { ... } int pop() { ... } }
-
栈的特殊访问顺序可以借助递归的性质来模拟。常用于各种括号匹配、中缀表达式转后缀表达式等问题。
例子:给定一个只包含
'('
和')'
的字符串,求最长有效括号子串的长度(最长有效括号)。我们利用栈去匹配括号,遇到'('
入栈,')'
出栈,最后计算长度。int longestValidParentheses(String s) { Stack<Integer> stack = new Stack<>(); stack.push(-1); // 哨兵,避免corner case int maxLen = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') stack.push(i); else { stack.pop(); // 出栈匹配 if (stack.isEmpty()) stack.push(i); // 直接入栈 else maxLen = Math.max(maxLen, i - stack.peek()); } } return maxLen; }
队列(Queue)
-
队列是一种先进先出(FIFO)的线性数据结构。Java提供了Queue接口及ArrayDeque等实现。
Queue<Integer> queue = new LinkedList<>(); queue.offer(1); // 入队 int front = queue.poll(); // 出队
-
队列常用于遍历或拓扑排序等操作。在广度优先搜索(BFS)算法中也经常用到队列保存待遍历的元素。
例子:打开转盘锁(锁的最小旋转次数)。我们可以利用BFS和队列,从初始状态出发,计算到达目标状态的最短路径。
int openLock(String[] deadends, String target) { Queue<String> queue = new LinkedList<>(); Set<String> visited = new HashSet<>(Arrays.asList(deadends)); queue.offer("0000"); visited.add("0000"); int step = 0; while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { //控制层次遍历,一次遍历所有当前队列的值 String curr = queue.poll(); if (target.equals(curr)) return step; //到达目标,返回步数 if (!visited.contains(curr)) { for (int j = 0; j < 4; j++) { String up = upOne(curr, j); String down = downOne(curr, j); if (!visited.contains(up)) { queue.offer(up); visited.add(up); } if (!visited.contains(down)) { queue.offer(down); visited.add(down); } } } } step++; } return -1; }
树(Tree)
-
树是一种分层数据的递归数据结构,常用的有二叉树、二叉查找树等。
// 二叉树节点定义 class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } }
-
树形结构广泛应用于文件系统、计算机网络、抽象语法树等,是一种非常重要的数据模型。二叉树、二叉查找树在算法竞赛中使用也非常普遍。
例子:给定两棵二叉树,编写一个函数来合并它们(合并二叉树)。
TreeNode mergeTrees(TreeNode t1, TreeNode t2) { if (t1 == null) return t2; if (t2 == null) return t1; TreeNode root = new TreeNode(t1.val + t2.val); root.left = mergeTrees(t1.left, t2.left); root.right = mergeTrees(t1.right, t2.right); return root; }
映射(Map)
-
映射是存储键值对数据的数据结构。Java中的HashMap基于散列实现,TreeMap基于红黑树实现。
Map<String, Integer> map = new HashMap<>(); map.put("A", 1); int val = map.get("A"); // 获取键对应的值
-
映射可以高效地存储键值对,常用于统计频率、建立关联关系等操作。
例子:给定两个字符串,找出其中最长的公共子序列长度(最长公共子序列)。我们可以利用动态规划和map存储计算过的结果。
int longestCommonSubsequence(String s1, String s2) { Map<String, Integer> memo = new HashMap<>(); return lcs(s1, 0, s2, 0, memo); } int lcs(String s1, int i, String s2, int j, Map<String, Integer> memo) { String key = i + "," + j; if (memo.containsKey(key)) return memo.get(key); int res = 0; if (i < s1.length() && j < s2.length() && s1.charAt(i) == s2.charAt(j)) res = 1 + lcs(s1, i + 1, s2, j + 1, memo); else res = Math.max(i < s1.length() ? lcs(s1, i + 1, s2, j, memo) : 0, j < s2.length() ? lcs(s1, i, s2, j + 1, memo) : 0); memo.put(key, res); return res; }
内置数据结构的快速使用
- 迭代器讲解
- Vector 容器(类)
- 线性表的使用
- 队列的使用
- 映射(map)的使用
- 集合(set)的使用
迭代器(Iterator)
在Java中,迭代器(Iterator)是一种用于遍历集合(Collection)元素的接口。它提供了一种统一的方式来访问不同集合类中的元素,而不用关心集合的底层实现细节。
迭代器接口定义如下:
public interface Iterator<E> {
boolean hasNext(); // 判断是否还有下一个元素
E next(); // 返回下一个元素
void remove(); // 移除当前元素(可选操作)
}
它主要有以下特点:
-
统一遍历方式:Iterator提供了统一的遍历方式,可以遍历各种不同的集合类,包括List、Set等。
-
单向移动指针:迭代器是单向移动指针,通过
next()
方法向集合中的下一个元素移动。 -
安全性:在单线程环境下,通过
Iterator
遍历集合时,不会抛出并发修改异常(ConcurrentModificationException
)。 -
可移除元素:迭代器可以通过
remove()
方法移除集合中的当前元素,但前提是在next()
之后、next()
之前没有调用过remove()
。 -
只能单向遍历:迭代器只能单向遍历,不能像List那样双向遍历。
下面是使用迭代器遍历一个List的示例:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
输出结果:
A
B
C
D
在Java中,可以通过Iterator
接口遍历任何实现了Iterable
接口的集合类。常见的集合类如ArrayList
、LinkedList
、HashSet
、TreeSet
等都实现了Iterable
接口,因此都可以使用迭代器进行遍历。
此外,Java还提供了ListIterator
接口扩展了Iterator
接口,增加了向前和向后遍历的能力,常用于List
集合的遍历和修改操作。
总之,迭代器为Java集合类提供了一种统一、安全、方便的遍历方式,是Java集合框架的重要组成部分。在算法竞赛中,熟练使用迭代器也可以提高代码的简洁性和可读性。
Vector 容器(类)
线性表中有 Vector
顺序表 和 list
链表,两者作用比较相似。
Vector
的主要作用就是可变长度的数组,就把他当成数组使用即可。
Vector
是Java集合框架中一种较老的动态数组实现,它和ArrayList
类似,但是Vector
是线程安全的(synchronized)。
Vector特点和用法:
-
线程安全
Vector
中的大部分方法都是同步的(synchronized),这意味着无论一个线程在读取或修改Vector
的时候,它都不会被其他线程所打扰,因此可以在多线程环境中安全地使用。Vector<String> vector = new Vector<>(); vector.add("A"); vector.add("B"); vector.add("C");
-
动态扩容
和ArrayList
类似,Vector
内部使用数组存储元素,当数组满了时会自动扩容,扩容操作耗费时间和内存。 -
允许null值
Vector
允许存储null
值。 -
迭代器和枚举
和其它集合一样,可以通过Iterator
遍历Vector
中的元素,也可以使用旧的Enumeration
枚举来遍历。Enumeration<String> e = vector.elements(); while(e.hasMoreElements()) { System.out.println(e.nextElement()); }
-
增删改查
Vector
提供了一系列增删改查的方法,如add()
、set()
、get()
、remove()
等。vector.remove(1); // 移除索引为1的元素 String val = vector.get(0); // 获取索引为0的元素
-
与
ArrayList
相比
由于Vector
是线程安全的,所以在单线程环境下会比ArrayList
慢一些。现在通常不直接使用Vector
,而是使用Collections.synchronizedList()
来包装一个ArrayList
使其线程安全:List<String> syncList = Collections.synchronizedList(new ArrayList<>());
以下为 Java Vector
的 Api
。
修饰符和类型 | 方法和说明 |
---|---|
boolean | add(E e)将指定的元素附加到此 Vector 的末尾。 |
void | add(int index, E element)在此 Vector 的指定位置插入指定元素。 |
boolean | addAll(Collection<? extends E> c)将指定集合中的所有元素追加到末尾 这个向量,按照它们由指定的返回的顺序 集合的迭代器。 |
boolean | addAll(int index, Collection<? extends E> c)将指定 Collection 中的所有元素插入到此 指定位置的向量。 |
void | addElement(E obj)将指定的组件添加到此向量的末尾, 将其大小增加一。 |
int | capacity()返回此向量的当前容量。 |
void | clear()从此 Vector 中删除所有元素。 |
Object | clone()返回此向量的克隆。 |
boolean | contains(Object o)退货 true 如果此向量包含指定的元素。 |
boolean | containsAll(Collection<?> c)如果此 Vector 包含所有元素,则返回 true 指定的集合。 |
void | copyInto Object [] anArray 将此向量的分量复制到指定的数组中。 |
E | elementAt(int index)返回指定索引处的组件。 |
Enumeration | elements()返回此向量的组件的枚举。 |
void | ensureCapacity(int minCapacity)如有必要,增加此向量的容量,以确保它至少可以容纳由指定的组件数量最小容量参数。 |
boolean | equals(Object o)比较指定的 Object 与此 Vector 是否相等。 |
E | firstElement()返回第一个组件(索引处的项目 0) 的这个向量。 |
E | get(int index)返回此 Vector 中指定位置的元素。 |
int | hashCode()返回此 Vector 的哈希码值。 |
int | indexOf(Object o)返回指定元素第一次出现的索引 在此向量中,如果此向量不包含该元素,则为 -1。 |
int | indexOf(Object o,int index)返回指定元素第一次出现的索引这个向量,从 index, 或返回 -1 如果 未找到该元素。 |
void | insertElementAt(E obj, int index)将指定对象作为组件插入此向量中的 指定的 index. |
boolean | isEmpty()测试此向量是否没有组件。 |
Iterator | iterator()以适当的顺序返回此列表中元素的迭代器 |
E | lastElement()返回向量的最后一个组件。 |
int | lastIndexOf(Object o)返回指定元素最后一次出现的索引在此向量中,如果此向量不包含该元素,则为 -1。 |
int | lastIndexOf(Object o, int index)返回指定元素最后一次出现的索引这个向量,从 index, 或返回 -1 如果 未找到该元素。 |
ListIterator | listIterator()返回此列表中元素的列表迭代器(在适当的顺序)。 |
ListIterator | listIterator(int index)返回此列表中元素的列表迭代器(在适当的序列),从列表中的指定位置开始。 |
E | remove(int index)移除此 Vector 中指定位置的元素。 |
boolean | remove(Object o)移除此 Vector 中第一次出现的指定元素如果 Vector 不包含该元素,则它保持不变。 |
boolean | removeAll(Collection<?> c)从此 Vector 中删除其包含在指定的集合。 |
void | removeAllElements()从此向量中删除所有组件并将其大小设置为零。 |
boolean | removeElement(Object obj)删除参数的第一个(最低索引)出现从这个向量。 |
void | removeElementAt(int index)删除指定索引处的组件。 |
protected void | removeRange(int fromIndex, int toIndex)从此列表中删除索引介于两者之间的所有元素 fromIndex,包括在内,和 toIndex, 独家的。 |
boolean | retainAll(Collection<?> c)仅保留此 Vector 中包含在指定的集合。 |
E | set(int index, E element)将此 Vector 中指定位置的元素替换为指定的元素。 |
void | setElementAt(E obj,int index)将组件设置在指定的位置 index 这个的向量是指定的对象。 |
void | setSize(int newSize)设置此向量的大小。 |
int | size()返回此向量中的组件数。 |
List | subList(int fromIndex,int toIndex)返回此列表中 fromIndex 之间的部分的视图 |
Object[] | toArray()返回一个包含此 Vector 中所有元素的数组以正确的顺序。 |
T[] | toArray[] 返回一个包含此 Vector 中所有元素的数组正确的顺序; 返回数组的运行时类型指定数组。 |
String | toString()返回此 Vector 的字符串表示形式,包含 每个元素的字符串表示。 |
void | trimToSize()将此向量的容量修剪为向量的电流 尺寸。 |
线性表基本使用
Java中的List
接口是java.util
包下的一部分,它是一个有序集合,用于存储元素的序列,这些元素可以重复。List
接口允许用户根据整数索引访问列表中的元素,提供了丰富的方法来插入、更新、删除和访问元素。
Java提供了ArrayList
、LinkedList
等几种List
实现。
顺序表随机访问O(1) ,随机访问事采用ArrayList
引入
在使用List
之前,需要导入java.util.List
接口,以及对应的实现类(如ArrayList
或LinkedList
):
import java.util.List;
import java.util.ArrayList;
// 或者使用 LinkedList
// import java.util.LinkedList;
创建List
创建一个List
实例通常使用其实现类,如ArrayList
或LinkedList
:
List<String> list = new ArrayList<>();
// 或者
// List<String> list = new LinkedList<>();
添加元素
使用add
方法向List
中添加元素:
list.add("Element 1");
list.add("Element 2");
list.add("Element 3");
访问元素
可以通过索引使用get
方法访问List
中的元素:
String element = list.get(1); // 访问第二个元素
System.out.println(element);
修改元素
使用set
方法可以修改List
中指定位置的元素:
list.set(1, "New Element 2"); // 将第二个元素修改为 "New Element 2"
删除元素
可以使用remove
方法通过索引或直接通过对象来删除元素:
list.remove(1); // 删除第二个元素
// 或者
list.remove("Element 3"); // 删除指定的对象
遍历List
List
可以通过for
循环或Iterator
来遍历:
// 使用 for 循环遍历
for(String item : list) {
System.out.println(item);
}
// 使用迭代器遍历
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String item = iterator.next();
System.out.println(item);
}
List大小
使用size
方法可以获取List
的大小:
int size = list.size();
System.out.println("List size: " + size);
List实现类
- ArrayList C++ vector: 底层使用动态数组实现,适用于频繁的随机访问和较少的插入、删除操作。
- LinkedList: C++ list底层使用双向链表实现,适用于频繁的插入、删除操作。
选择哪种实现类取决于具体的使用场景。ArrayList通常是List实现的首选,除非程序特别需要LinkedList的特定功能(如频繁的插入和删除)。
队列 Queue
定义方式
Queue<String> queue = new LinkedList<String>();
部分成员函数(包括继承的):
add()
: 增加一个元索,如果队列已满,则抛出一个异常remove()
:移除并返回队列头部的元素,如果队列为空,则抛出一个异常element()
:返回队列头部的元素,如果队列为空,则抛出一个异常offer()
:添加一个元素并返回true
,如果队列已满,则返回false
poll()
: 移除并返问队列头部的元素,如果队列为空,则返回null
peek()
: 返回队列头部的元素,如果队列为空,则返回null
put()
: 添加一个元素, 如果队列满,则阻塞take()
: 移除并返回队列头部的元素,如果队列为空,则阻塞size()
: 返回队列长度。
基本使用
offer(E e)
和add(E e)
Queue<String> queue = new LinkedList<>();
queue.offer("A"); // 返回true
queue.add("B"); // 不会抛出异常
queue.offer("C");
queue.offer("D");
System.out.println(queue); // 输出 [A, B, C, D]
Queue<String> queueFull = new LinkedList<>(Arrays.asList("1", "2", "3"));
queueFull.add("4"); // 抛出 IllegalStateException
poll()
和remove()
Queue<String> queue = new LinkedList<>(Arrays.asList("A", "B", "C"));
System.out.println(queue.poll()); // 输出 A
System.out.println(queue.remove()); // 输出 B
System.out.println(queue.poll()); // 输出 C
System.out.println(queue.poll()); // 输出 null
System.out.println(queue.remove()); // 抛出 NoSuchElementException
peek()
和element()
Queue<String> queue = new LinkedList<>(Arrays.asList("A", "B", "C"));
System.out.println(queue.peek()); // 输出 A
System.out.println(queue.element()); // 输出 A
System.out.println(queue.peek()); // 输出 A
Queue<String> emptyQueue = new LinkedList<>();
System.out.println(emptyQueue.peek()); // 输出 null
System.out.println(emptyQueue.element()); // 抛出 NoSuchElementException
put(E e)
和take()
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(2);
blockingQueue.put("A"); // 不会阻塞
blockingQueue.put("B"); // 不会阻塞
blockingQueue.put("C"); // 会阻塞,直到队列有空位
new Thread(() -> {
try {
Thread.sleep(1000); // 休眠1秒
blockingQueue.take(); // 移除队首元素
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
blockingQueue.put("D"); // 由于上面的take()操作,此处不会阻塞
contains(Object o)
和size()
Queue<String> queue = new LinkedList<>(Arrays.asList("A", "B", "C", "D"));
System.out.println(queue.contains("A")); // 输出 true
System.out.println(queue.contains("E")); // 输出 false
System.out.println(queue.size()); // 输出 4
Map 映射
map
是一个关联容器,它提供一对一的 hash
。
- 第一个可以称为关键字(
key
),每个关键字只能在map
中出现一次 - 第二个可能称为该关键字的值(
value
)
map
以模板(泛型)方式实现,可以存储任意类型的数据,包括使用者自定义的数据类型。Map
主要用于资料一对一映射(one-to-one
)的情況,map
在 C++
的內部的实现自建一颗红黑树,这颗树具有对数据自动排序的功能。在 map
内部所有的数据都是有序的。
比如,像是管理班级内的学生,Key
值为学号,Value
放其他信息的结构体或者类。
定义方法
Map m1 = new TreeMap();
这里我们讲的是排序的 map
还有不排序的 map
,java
里面叫 hashmap
,C++
里叫 unordered_map
,除了不排序,用法和功能都一样。
基本使用
Java中的Map
是一种键值对(Key-Value)映射结构的集合接口,它提供了一种存储和检索值的便捷方式。Map
中的每个元素都由一个键(Key)和一个值(Value)组成,键用于唯一标识一个值,值则是与该键相关联的数据。Map
接口定义了一些常用的方法,用于操作键值对的存储和访问。下面是Map
接口的基本使用:
-
创建Map实例
Java提供了多种
Map
接口的实现类,如HashMap
、LinkedHashMap
、TreeMap
等。我们可以使用相应的构造方法创建一个Map
实例。// 创建一个空的HashMap Map<String, Integer> map = new HashMap<>(); // 创建一个初始容量为8的HashMap Map<String, Integer> map = new HashMap<>(8); // 使用静态工厂方法创建一个不可修改的Map Map<String, Integer> map = Map.of("A", 1, "B", 2, "C", 3);
-
添加键值对
使用
put()
方法向Map
中添加新的键值对。如果键已经存在,则会用新值替换旧值。map.put("Apple", 3); map.put("Banana", 5); map.put("Orange", 2);
-
获取值
使用
get()
方法根据键获取对应的值。如果键不存在,则返回null
。Integer count = map.get("Apple"); // 3 Integer missing = map.get("Grape"); // null
-
移除键值对
使用
remove()
方法根据键移除对应的键值对,并返回被移除的值。Integer removed = map.remove("Banana"); // 5
-
检查是否包含键或值
使用
containsKey()
和containsValue()
方法检查Map
中是否包含指定的键或值。boolean hasApple = map.containsKey("Apple"); // true boolean hasFive = map.containsValue(5); // false
-
迭代访问
使用
keySet()
、values()
和entrySet()
方法分别获取Map
中所有键、值或键值对的集合视图,然后使用迭代器或增强for循环遍历。// 遍历所有键 for (String key : map.keySet()) { System.out.println(key); } // 遍历所有值 for (Integer value : map.values()) { System.out.println(value); } // 遍历所有键值对 for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ":" + entry.getValue()); }
-
其他常用方法
Map
接口还提供了一些其他常用方法,如size()
获取映射大小、isEmpty()
判断是否为空、putAll()
将另一个Map
的所有映射关系添加到当前Map
等。
成员方法
方法名 | 方法描述 |
---|---|
void clear( ) | 从此映射中移除所有映射关系(可选操作)。 |
boolean containsKey(Object k) | 如果此映射包含指定键的映射关系,则返回 true。 |
boolean containsValue(Object v) | 如果此映射将一个或多个键映射到指定值,则返回 true。 |
boolean equals(Object obj) | 比较指定的对象与此映射是否相等。 |
Object get(Object x) | 返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。 |
int hashCode( ) | 返回此映射的哈希码值。 |
boolean isEmpty( ) | 如果此映射未包含键-值映射关系,则返回 true。 |
Set keySet( ) | 返回此映射中包含的键的 Set 视图。 |
Object put(Object k, Object v) | 将指定的值与此映射中的指定键关联 |
Object remove(Object k) | 如果存在一个键的映射关系,则将其从此映射中移除(可选操作)。 |
int size( ) | 返回此映射中的键-值映射关系数。 |
Collection values( ) | 返回此映射中包含的值的 Collection 视图。 |
Set 集合使用
定义方法
Set是一种不包含重复元素的集合,它继承自Collection接口。
Java提供了两个主要的Set实现:HashSet和TreeSet。
-
HashSet
HashSet是基于哈希表实现的,它不保证元素的顺序,允许使用null元素。HashSet的主要特点是:
- 不允许存储重复元素
- 元素无序
- 非线程安全
- 允许null元素
- 性能较好,查找效率高
示例:
Set<String> set = new HashSet<>(); set.add("apple"); set.add("banana"); set.add("apple"); // 重复元素被忽略 System.out.println(set); // 输出 [banana, apple]
-
TreeSet
TreeSet是基于红黑树实现的,它会自动对元素进行排序。TreeSet的主要特点是:
- 不允许存储重复元素
- 元素自动排序
- 非线程安全
- 不允许null元素
- 性能稍逊于HashSet
示例:
Set<Integer> set = new TreeSet<>(); set.add(3); set.add(1); set.add(2); set.add(3); // 重复元素被忽略 System.out.println(set); // 输出 [1, 2, 3]
Set集合常用于需要去除重复元素的场景,如数学集合运算、词频统计等。除了HashSet和TreeSet之外,Java还提供了一些特殊的Set实现,如LinkedHashSet(有序Set)、CopyOnWriteArraySet(线程安全Set)等。
基本使用
-
创建Set实例
// HashSet Set<String> set1 = new HashSet<>(); // TreeSet Set<Integer> set2 = new TreeSet<>();
-
添加元素
使用add()方法添加元素到Set中,添加重复元素时会被忽略。
set1.add("apple"); set1.add("banana"); set1.add("apple"); // 会被忽略 set2.add(3); set2.add(1); set2.add(2); set2.add(3); // 会被忽略
-
删除元素
使用remove()方法删除指定元素。
set1.remove("apple"); // 删除"apple"元素
-
判断元素是否存在
使用contains()方法判断Set中是否包含指定元素。
boolean containsApple = set1.contains("apple");
-
获取Set大小
使用size()方法获取Set的元素个数。
int size = set1.size();
-
Set遍历
可以使用增强for循环或迭代器遍历Set中的元素。
// 增强for循环 for (String s : set1) { System.out.println(s); } // 迭代器遍历 Iterator<Integer> iter = set2.iterator(); while (iter.hasNext()) { System.out.println(iter.next()); }
-
Set运算
Set提供了一些方法用于进行集合运算,如
addAll()
(并集)、retainAll()
(交集)、removeAll()
(差集)等。Set<String> set3 = new HashSet<>(Arrays.asList("apple", "orange")); set1.addAll(set3); // 并集 set1.retainAll(set3); // 交集 set1.removeAll(set3); // 差集
-
Set转换为数组
使用
toArray()
方法可以将Set转换为数组。String[] arr = set1.toArray(new String[0]);
自定义排序准则
要使用自定义排序准则,你可以定义一个比较函数或者更常见的是定义一个比较类(比较函数对象),然后将其作为第二个模板参数传递给set。
使用Comparator进行自定义排序
定义一个比较类通常更灵活,因为它可以持有状态(虽然在大多数排序场景中不需要)。
在Java中,Set
接口的实现类(如HashSet
)本身不保证有序,但Java提供了SortedSet
接口和TreeSet
实现类,后者可以根据自然排序或提供的Comparator
进行排序。若要对Set
中的元素应用自定义排序准则,可以使用TreeSet
并提供一个自定义的Comparator
。
要在TreeSet
中实现自定义排序,可以在创建TreeSet
时通过构造器传入一个Comparator
对象。Comparator
接口定义了一个compare
方法,你需要在其中实现你的排序逻辑。
下面是一个简单的例子,展示如何根据字符串的长度而不是字典顺序来排序String
元素:
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
public class CustomSortExample {
public static void main(String[] args) {
// 使用Comparator实现自定义排序逻辑
Comparator<String> stringLengthComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
};
// 创建一个TreeSet并传入自定义的Comparator
Set<String> strings = new TreeSet<>(stringLengthComparator);
strings.add("Apple");
strings.add("Fig");
strings.add("Banana");
for (String s : strings) {
System.out.println(s);
}
// 输出结果将根据字符串长度排序:Fig, Apple, Banana
}
}
在这个例子中,Comparator
实现了根据字符串长度的比较逻辑,然后我们将这个Comparator
传递给TreeSet
的构造函数。这样,TreeSet
就会根据字符串长度来排序存储的元素。
使用Lambda表达式简化Comparator:
如果你使用的是Java 8或更高版本,可以使用Lambda表达式来更简洁地实现Comparator
:
Set<String> strings = new TreeSet<>((s1, s2) -> Integer.compare(s1.length(), s2.length()));
这行代码与上面的例子实现了相同的功能,但是更简洁。
例题练习
Vector 的使用例题
- 输入
10
10124214 北京
12421565 上海
sdafasdg213 天津
fasdfga124 北京
145252 上海
235wtdfsg 济南
3242356fgdfsg 成都
23423 武汉
23423565f 沈阳
1245dfwfs 成都
- 输出
北京 2
10124214
fasdfga124
上海 2
12421565
145252
天津 1
sdafasdg213
济南 1
235wtdfsg
成都 2
3242356fgdfsg
1245dfwfs
武汉 1
23423
沈阳 1
23423565f
解题思路:
首先我们要知道中国城市肯定在 1000 个以内,但是单号我们不确定,我们不可能每个数组开 1000000 个,那样内存不够,所以这时候我们就用到了我们的vector
,他的容量是动态申请的,在比赛中我们可以理解为无限制。
- 第一步:我们创建一个
vector
用于保存地址
vector<string> city;
- 第二步:我们创建一个
vector
组用于存放单号
vector<string> dig[1000];
- 第三步:我们定义一个映射函数,因为你的城市可能会再次出现,你需要知道之前有没有。
- 第四步:我们开始读入操作并按照顺序进行存放
解题步骤:
- 输入一些城市和对应的车站名称。
- 如果城市已存在,则将车站添加到对应城市的列表中;如果城市不存在,则先添加城市,再为该城市创建一个车站列表。
- 最后输出每个城市的名称、该城市的车站数量,以及每个车站的名称。
使用了两个Vector对象来存储城市名称和对应的车站列表。Myfind
函数用于查找给定城市在city
向量中的位置,如果找到则返回索引,否则返回-1。
在main
函数中,首先输入城市数量n
,然后循环输入每个城市及其对应的车站名称。对于每个输入的城市,先调用Myfind
查找是否已存在,如果存在则将车站添加到对应列表,否则先添加城市,再为其创建一个新的车站列表。
最后,遍历city
和dig
向量,输出每个城市的名称、车站数量和每个车站的名称。
import java.util.Scanner;
import java.util.Vector;
public class Main {
static Vector<String> city = new Vector<String>(); // 存储城市
static Vector<Vector<String>> dig = new Vector<Vector<String>>(); // 用于存储对应城市的快递单号
static int Myfind(String s) { // 查找给定城市在city中的位置
for (int i = 0; i < city.size(); i++) {
if (city.get(i).equals(s)) {
return i; // 如果找到,返回索引
}
}
return -1; // 如果没找到,返回-1
}
public static void main(String[] args) {
int n;
Scanner in = new Scanner(System.in);
n = in.nextInt(); // 输入城市数量n
for (int i = 0; i < n; i++) {
String d, c;
d = in.next(); // 输入快递单号
c = in.next(); // 输入城市名称
int flag = Myfind(c); // 查找城市在city向量中的位置
if (flag == -1) { // 如果城市不存在
city.addElement(c); // 将城市添加到city向量
dig.addElement(new Vector<String>()); // 为新城市创建一个快递单号列表
dig.get(city.size() - 1).addElement(d); // 将快递单号添加到对应城市的列表
} else {
dig.get(flag).addElement(d); // 如果城市存在,将单号添加到对应城市的列表
}
}
for (int i = 0; i < city.size(); i++) {
System.out.println(city.get(i) + " " + dig.get(i).size());
for (int j = 0; j < dig.get(i).size(); j++) {
System.out.println(dig.get(i).get(j));
}
}
}
}
队列使用例题
- 输入
5
IN xiaoming N
IN Adel V
IN laozhao N
OUT N
IN CLZ V
- 输出
Adel
CLZ
laozhao
解题思路:
- 用两个队列分别存储VIP用户和普通用户。
- 根据输入的指令操作两个队列,IN指令表示入队,OUT指令表示出队。
- 最后分别遍历两个队列,按顺序输出用户名字。
解题步骤:
- 创建两个队列V和N,分别用于存储VIP用户和普通用户。
- 读入指令的个数M。
- 对于每个指令:
- 如果指令是"IN name V",则将name入队到V队列。
- 如果指令是"IN name N",则将name入队到N队列。
- 如果指令是"OUT V",则将V队列队首元素出队。
- 如果指令是"OUT N",则将N队列队首元素出队。
- 遍历V队列,依次输出其中的元素。
- 遍历N队列,依次输出其中的元素。
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class Main {
static Queue<String> V = new LinkedList<String>(); // 创建VIP队列
static Queue<String> N = new LinkedList<String>(); // 创建普通队列
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int M = in.nextInt(); // 读入指令个数
while (M > 0) { // 处理每个指令
M--;
String op, name, type;
op = in.next(); // 读入指令
if (op.contains("IN")) { // 如果是入队指令
name = in.next(); // 读入用户名
type = in.next(); // 读入用户类型
if (type.contains("V")) {
V.offer(name); // 将用户名入队到VIP队列
} else {
N.offer(name); // 将用户名入队到普通队列
}
} else { // 如果是出队指令
type = in.next(); // 读入用户类型
if (type.contains("V")) {
V.poll(); // 将VIP队列队首元素出队
} else {
N.poll(); // 将普通队列队首元素出队
}
}
}
while (V.size() != 0) { // 输出VIP队列
System.out.println(V.poll());
}
while (N.size() != 0) { // 输出普通队列
System.out.println(N.poll());
}
}
}
这段代码首先创建了两个队列V和N,用于分别存储VIP用户和普通用户。然后读入指令的个数M,进入循环处理每个指令。对于入队指令(“IN name V"或"IN name N”),将用户名字入队到对应的队列;对于出队指令(“OUT V"或"OUT N”),将对应队列的队首元素出队。最后,分别遍历V和N队列,依次输出其中的元素。
Map使用例题
- 输入
6
1fagas
dsafa32j
lkiuopybncv
hfgdjytr
cncxfg
sdhrest
- 输出
NO
- 输入
5
sdfggfds
fgsdhsdf
dsfhsdhr
sdfhdfh
sdfggfds
- 输出
sdfggfds
解题思路:
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
public class Main {
// 声明一个静态的 TreeMap 来存储已经遇到的唯一单词
static Map<String, Boolean> mp = new TreeMap<>();
public static void main(String[] args) {
int n;
boolean flag = false; // 标志变量,用于指示是否发现重复单词
Scanner in = new Scanner(System.in);
String ans = "NO"; // 如果没有发现重复单词,则默认答案为 "NO"
n = in.nextInt();
// 遍历每个输入单词
for (int i = 0; i < n; i++) {
String word;
word = in.next(); // 读取下一个单词
// 如果已经发现了重复单词,则跳过后续处理
if (flag)
continue;
// 检查单词是否已经遇到过
if (mp.containsKey(word)) {
flag = true; // 将标志设置为 true,表示已经发现重复单词
ans = word; // 存储重复的单词
} else {
mp.put(word, true); // 将单词添加到 map 中,值为 true 表示已经遇到过
}
}
// 输出结果(要么是重复的单词,要么是 "NO" 如果没有发现重复)
System.out.println(ans);
}
}