蓝桥杯第十五届抱佛脚(二)竞赛中的数据结构

蓝桥杯第十五届抱佛脚(二)内置数据结构

文章目录

在竞赛中常见的数据结构

数组(Array)

  • 数组是一种线性数据结构,在内存中是一块连续的存储空间。可以通过索引随机访问元素,查找效率很高。但数组大小固定,插入和删除操作效率低下。

    java 复制代码
    int[] arr = new int[10]; // 声明一个长度为10的整型数组
    arr[0] = 1; // 访问和赋值
  • 数组支持随机访问,查找效率高,适合存储基础数据类型和需要频繁查询的数据。但插入和删除效率低。

    例子:给定一个数组,求数组中是否有两个数之和等于目标值(两数之和)。我们可以先排序,然后使用双指针从首尾向中间扫描,时间复杂度O(nlogn)。

    java 复制代码
    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)

  • 链表是一种动态数据结构,每个节点包含数据和指向下一个节点的引用。优点是内存动态分配,插入删除效率高,缺点是访问元素需要遍历,查找效率低。

    java 复制代码
    // 单链表节点定义
    class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
    }
  • 链表适合需要频繁插入和删除的场景。单链表无法高效查找和访问指定位置的元素,双向链表可以有效解决这个问题。

    例子:反转一个单链表(链表反转)。我们可以使用迭代或递归的方式进行反转。

    java 复制代码
    // 迭代反转单链表
    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中可以用数组或链表实现。

    java 复制代码
    // 基于数组实现的栈
    class ArrayStack {
      int[] arr = new int[100]; // 存储元素的数组
      int top = -1; // 栈顶指针
      void push(int x) { ... }
      int pop() { ... }
    }
  • 栈的特殊访问顺序可以借助递归的性质来模拟。常用于各种括号匹配、中缀表达式转后缀表达式等问题。

    例子:给定一个只包含'('')'的字符串,求最长有效括号子串的长度(最长有效括号)。我们利用栈去匹配括号,遇到'('入栈,')'出栈,最后计算长度。

    java 复制代码
    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等实现。

    java 复制代码
    Queue<Integer> queue = new LinkedList<>();
    queue.offer(1); // 入队
    int front = queue.poll(); // 出队
  • 队列常用于遍历或拓扑排序等操作。在广度优先搜索(BFS)算法中也经常用到队列保存待遍历的元素。

    例子:打开转盘锁(锁的最小旋转次数)。我们可以利用BFS和队列,从初始状态出发,计算到达目标状态的最短路径。

    java 复制代码
    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)

  • 树是一种分层数据的递归数据结构,常用的有二叉树、二叉查找树等。

    java 复制代码
    // 二叉树节点定义
    class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
    }
  • 树形结构广泛应用于文件系统、计算机网络、抽象语法树等,是一种非常重要的数据模型。二叉树、二叉查找树在算法竞赛中使用也非常普遍。

    例子:给定两棵二叉树,编写一个函数来合并它们(合并二叉树)。

    java 复制代码
    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基于红黑树实现。

    java 复制代码
    Map<String, Integer> map = new HashMap<>();
    map.put("A", 1);
    int val = map.get("A"); // 获取键对应的值
  • 映射可以高效地存储键值对,常用于统计频率、建立关联关系等操作。

    例子:给定两个字符串,找出其中最长的公共子序列长度(最长公共子序列)。我们可以利用动态规划和map存储计算过的结果。

    java 复制代码
    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)元素的接口。它提供了一种统一的方式来访问不同集合类中的元素,而不用关心集合的底层实现细节。

迭代器接口定义如下:

java 复制代码
public interface Iterator<E> {
    boolean hasNext(); // 判断是否还有下一个元素
    E next(); // 返回下一个元素
    void remove(); // 移除当前元素(可选操作)
}

它主要有以下特点:

  1. 统一遍历方式:Iterator提供了统一的遍历方式,可以遍历各种不同的集合类,包括List、Set等。

  2. 单向移动指针 :迭代器是单向移动指针,通过next()方法向集合中的下一个元素移动。

  3. 安全性 :在单线程环境下,通过Iterator遍历集合时,不会抛出并发修改异常(ConcurrentModificationException)。

  4. 可移除元素 :迭代器可以通过remove()方法移除集合中的当前元素,但前提是在next()之后、next()之前没有调用过remove()

  5. 只能单向遍历:迭代器只能单向遍历,不能像List那样双向遍历。

下面是使用迭代器遍历一个List的示例:

java 复制代码
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接口的集合类。常见的集合类如ArrayListLinkedListHashSetTreeSet等都实现了Iterable接口,因此都可以使用迭代器进行遍历。

此外,Java还提供了ListIterator接口扩展了Iterator接口,增加了向前和向后遍历的能力,常用于List集合的遍历和修改操作。

总之,迭代器为Java集合类提供了一种统一、安全、方便的遍历方式,是Java集合框架的重要组成部分。在算法竞赛中,熟练使用迭代器也可以提高代码的简洁性和可读性。

Vector 容器(类)

线性表中有 Vector 顺序表 和 list 链表,两者作用比较相似。

Vector 的主要作用就是可变长度的数组,就把他当成数组使用即可。

Vector是Java集合框架中一种较老的动态数组实现,它和ArrayList类似,但是Vector是线程安全的(synchronized)。

Vector特点和用法:

  1. 线程安全
    Vector中的大部分方法都是同步的(synchronized),这意味着无论一个线程在读取或修改Vector的时候,它都不会被其他线程所打扰,因此可以在多线程环境中安全地使用。

    java 复制代码
    Vector<String> vector = new Vector<>();
    vector.add("A");
    vector.add("B");
    vector.add("C");
  2. 动态扩容

    ArrayList类似,Vector内部使用数组存储元素,当数组满了时会自动扩容,扩容操作耗费时间和内存。

  3. 允许null值
    Vector允许存储null值。

  4. 迭代器和枚举

    和其它集合一样,可以通过Iterator遍历Vector中的元素,也可以使用旧的Enumeration枚举来遍历。

    java 复制代码
    Enumeration<String> e = vector.elements();
    while(e.hasMoreElements()) {
        System.out.println(e.nextElement());
    }
  5. 增删改查
    Vector提供了一系列增删改查的方法,如add()set()get()remove()等。

    java 复制代码
    vector.remove(1); // 移除索引为1的元素
    String val = vector.get(0); // 获取索引为0的元素
  6. ArrayList相比

    由于Vector是线程安全的,所以在单线程环境下会比ArrayList慢一些。现在通常不直接使用Vector,而是使用Collections.synchronizedList()来包装一个ArrayList使其线程安全:

    java 复制代码
    List<String> syncList = Collections.synchronizedList(new ArrayList<>());

以下为 Java VectorApi

修饰符和类型 方法和说明
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提供了ArrayListLinkedList等几种List实现。

顺序表随机访问O(1) ,随机访问事采用ArrayList

引入

在使用List之前,需要导入java.util.List接口,以及对应的实现类(如ArrayListLinkedList):

java 复制代码
import java.util.List;
import java.util.ArrayList;
// 或者使用 LinkedList
// import java.util.LinkedList;
创建List

创建一个List实例通常使用其实现类,如ArrayListLinkedList

java 复制代码
List<String> list = new ArrayList<>();
// 或者
// List<String> list = new LinkedList<>();
添加元素

使用add方法向List中添加元素:

java 复制代码
list.add("Element 1");
list.add("Element 2");
list.add("Element 3");
访问元素

可以通过索引使用get方法访问List中的元素:

java 复制代码
String element = list.get(1); // 访问第二个元素
System.out.println(element);
修改元素

使用set方法可以修改List中指定位置的元素:

java 复制代码
list.set(1, "New Element 2"); // 将第二个元素修改为 "New Element 2"
删除元素

可以使用remove方法通过索引或直接通过对象来删除元素:

java 复制代码
list.remove(1); // 删除第二个元素
// 或者
list.remove("Element 3"); // 删除指定的对象
遍历List

List可以通过for循环或Iterator来遍历:

java 复制代码
// 使用 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的大小:

java 复制代码
int size = list.size();
System.out.println("List size: " + size);
List实现类
  • ArrayList C++ vector: 底层使用动态数组实现,适用于频繁的随机访问和较少的插入、删除操作。
  • LinkedList: C++ list底层使用双向链表实现,适用于频繁的插入、删除操作。

选择哪种实现类取决于具体的使用场景。ArrayList通常是List实现的首选,除非程序特别需要LinkedList的特定功能(如频繁的插入和删除)。

队列 Queue

定义方式
java 复制代码
Queue<String> queue = new LinkedList<String>();

部分成员函数(包括继承的):

  • add(): 增加一个元索,如果队列已满,则抛出一个异常
  • remove():移除并返回队列头部的元素,如果队列为空,则抛出一个异常
  • element():返回队列头部的元素,如果队列为空,则抛出一个异常
  • offer():添加一个元素并返回 true,如果队列已满,则返回 false
  • poll(): 移除并返问队列头部的元素,如果队列为空,则返回 null
  • peek(): 返回队列头部的元素,如果队列为空,则返回 null
  • put(): 添加一个元素, 如果队列满,则阻塞
  • take(): 移除并返回队列头部的元素,如果队列为空,则阻塞
  • size(): 返回队列长度。
基本使用
  1. offer(E e)add(E e)
java 复制代码
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
  1. poll()remove()
java 复制代码
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
  1. peek()element()
java 复制代码
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
  1. put(E e)take()
java 复制代码
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()操作,此处不会阻塞
  1. contains(Object o)size()
java 复制代码
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)的情況,mapC++ 的內部的实现自建一颗红黑树,这颗树具有对数据自动排序的功能。在 map 内部所有的数据都是有序的。

比如,像是管理班级内的学生,Key 值为学号,Value 放其他信息的结构体或者类。

定义方法
java 复制代码
Map m1 = new TreeMap();

这里我们讲的是排序的 map 还有不排序的 mapjava 里面叫 hashmapC++ 里叫 unordered_map,除了不排序,用法和功能都一样。

基本使用

Java中的Map是一种键值对(Key-Value)映射结构的集合接口,它提供了一种存储和检索值的便捷方式。Map中的每个元素都由一个键(Key)和一个值(Value)组成,键用于唯一标识一个值,值则是与该键相关联的数据。Map接口定义了一些常用的方法,用于操作键值对的存储和访问。下面是Map接口的基本使用:

  1. 创建Map实例

    Java提供了多种Map接口的实现类,如HashMapLinkedHashMapTreeMap等。我们可以使用相应的构造方法创建一个Map实例。

    java 复制代码
    // 创建一个空的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);
  2. 添加键值对

    使用put()方法向Map中添加新的键值对。如果键已经存在,则会用新值替换旧值。

    java 复制代码
    map.put("Apple", 3);
    map.put("Banana", 5);
    map.put("Orange", 2);
  3. 获取值

    使用get()方法根据键获取对应的值。如果键不存在,则返回null

    java 复制代码
    Integer count = map.get("Apple"); // 3
    Integer missing = map.get("Grape"); // null
  4. 移除键值对

    使用remove()方法根据键移除对应的键值对,并返回被移除的值。

    java 复制代码
    Integer removed = map.remove("Banana"); // 5
  5. 检查是否包含键或值

    使用containsKey()containsValue()方法检查Map中是否包含指定的键或值。

    java 复制代码
    boolean hasApple = map.containsKey("Apple"); // true
    boolean hasFive = map.containsValue(5); // false
  6. 迭代访问

    使用keySet()values()entrySet()方法分别获取Map中所有键、值或键值对的集合视图,然后使用迭代器或增强for循环遍历。

    java 复制代码
    // 遍历所有键
    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());
    }
  7. 其他常用方法

    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。

  1. HashSet

    HashSet是基于哈希表实现的,它不保证元素的顺序,允许使用null元素。HashSet的主要特点是:

    • 不允许存储重复元素
    • 元素无序
    • 非线程安全
    • 允许null元素
    • 性能较好,查找效率高

    示例:

    java 复制代码
    Set<String> set = new HashSet<>();
    set.add("apple");
    set.add("banana");
    set.add("apple"); // 重复元素被忽略
    System.out.println(set); // 输出 [banana, apple]
  2. TreeSet

    TreeSet是基于红黑树实现的,它会自动对元素进行排序。TreeSet的主要特点是:

    • 不允许存储重复元素
    • 元素自动排序
    • 非线程安全
    • 不允许null元素
    • 性能稍逊于HashSet

    示例:

    java 复制代码
    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)等。

基本使用
  1. 创建Set实例

    java 复制代码
    // HashSet
    Set<String> set1 = new HashSet<>();
    
    // TreeSet
    Set<Integer> set2 = new TreeSet<>();
  2. 添加元素

    使用add()方法添加元素到Set中,添加重复元素时会被忽略。

    java 复制代码
    set1.add("apple");
    set1.add("banana");
    set1.add("apple"); // 会被忽略
    
    set2.add(3);
    set2.add(1);
    set2.add(2);
    set2.add(3); // 会被忽略
  3. 删除元素

    使用remove()方法删除指定元素。

    java 复制代码
    set1.remove("apple"); // 删除"apple"元素
  4. 判断元素是否存在

    使用contains()方法判断Set中是否包含指定元素。

    java 复制代码
    boolean containsApple = set1.contains("apple");
  5. 获取Set大小

    使用size()方法获取Set的元素个数。

    java 复制代码
    int size = set1.size();
  6. Set遍历

    可以使用增强for循环或迭代器遍历Set中的元素。

    java 复制代码
    // 增强for循环
    for (String s : set1) {
        System.out.println(s);
    }
    
    // 迭代器遍历
    Iterator<Integer> iter = set2.iterator();
    while (iter.hasNext()) {
        System.out.println(iter.next());
    }
  7. Set运算

    Set提供了一些方法用于进行集合运算,如 addAll()(并集)、retainAll()(交集)、removeAll()(差集)等。

    java 复制代码
    Set<String> set3 = new HashSet<>(Arrays.asList("apple", "orange"));
    set1.addAll(set3); // 并集
    set1.retainAll(set3); // 交集  
    set1.removeAll(set3); // 差集
  8. Set转换为数组

    使用toArray()方法可以将Set转换为数组。

    java 复制代码
    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元素:

java 复制代码
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

java 复制代码
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 用于保存地址
cpp 复制代码
vector<string> city;
  • 第二步:我们创建一个 vector 组用于存放单号
cpp 复制代码
vector<string> dig[1000];
  • 第三步:我们定义一个映射函数,因为你的城市可能会再次出现,你需要知道之前有没有。
  • 第四步:我们开始读入操作并按照顺序进行存放

解题步骤:

  1. 输入一些城市和对应的车站名称。
  2. 如果城市已存在,则将车站添加到对应城市的列表中;如果城市不存在,则先添加城市,再为该城市创建一个车站列表。
  3. 最后输出每个城市的名称、该城市的车站数量,以及每个车站的名称。

使用了两个Vector对象来存储城市名称和对应的车站列表。Myfind函数用于查找给定城市在city向量中的位置,如果找到则返回索引,否则返回-1。

main函数中,首先输入城市数量n,然后循环输入每个城市及其对应的车站名称。对于每个输入的城市,先调用Myfind查找是否已存在,如果存在则将车站添加到对应列表,否则先添加城市,再为其创建一个新的车站列表。

最后,遍历citydig向量,输出每个城市的名称、车站数量和每个车站的名称。

java 复制代码
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

解题思路:

  1. 用两个队列分别存储VIP用户和普通用户。
  2. 根据输入的指令操作两个队列,IN指令表示入队,OUT指令表示出队。
  3. 最后分别遍历两个队列,按顺序输出用户名字。

解题步骤:

  1. 创建两个队列V和N,分别用于存储VIP用户和普通用户。
  2. 读入指令的个数M。
  3. 对于每个指令:
    • 如果指令是"IN name V",则将name入队到V队列。
    • 如果指令是"IN name N",则将name入队到N队列。
    • 如果指令是"OUT V",则将V队列队首元素出队。
    • 如果指令是"OUT N",则将N队列队首元素出队。
  4. 遍历V队列,依次输出其中的元素。
  5. 遍历N队列,依次输出其中的元素。
java 复制代码
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

解题思路:

java 复制代码
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);
    }
}
相关推荐
AI街潜水的八角5 分钟前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
白榆maple30 分钟前
(蓝桥杯C/C++)——基础算法(下)
算法
JSU_曾是此间年少34 分钟前
数据结构——线性表与链表
数据结构·c++·算法
sjsjs1141 分钟前
【数据结构-合法括号字符串】【hard】【拼多多面试题】力扣32. 最长有效括号
数据结构·leetcode
此生只爱蛋1 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构
昂子的博客2 小时前
基础数据结构——队列(链表实现)
数据结构
咕咕吖2 小时前
对称二叉树(力扣101)
算法·leetcode·职场和发展
九圣残炎3 小时前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
lulu_gh_yu3 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法