Java集合框架
Java集合类主要由两个根接口Collection和Map派生出来的。
Collection派生出了三个子接口:
1)List
List代表了有序可重复集合,可直接根据元素的索引来访问
2)Set
Set代表无序不可重复集合,只能根据元素本身来访问
3)Queue
Queue是队列集合
Map接口派生:
Map代表的是存储key-value对的集合,可根据元素的key来访问value。
因此Java集合大致也可分成List、Set、Queue、Map四种接口体系。
Map
和 Collection
之间有一些联系,同时它们也有一些明显的区别。以下是它们之间的联系和区别:
联系:
-
都是集合框架的一部分:
Map
和Collection
都是 Java 集合框架的接口之一,它们都用于存储和组织数据。 -
都支持泛型: 从 Java 5 开始,
Map
和Collection
都支持泛型,以提供类型安全的集合。 -
都有迭代器:
Map
和Collection
都提供了迭代器(Iterator)的机制,可以用来遍历其中的元素。
区别:
-
存储方式:
- Map: 存储的是键值对,每个键对应一个值。
- Collection: 存储的是独立的元素,每个元素都是一个单独的对象。
-
元素唯一性:
- Map: 键是唯一的,值可以不唯一。
-
Collection: 取决于具体的实现类,
Set
要求元素唯一,而List
和Queue
允许元素重复。
-
获取元素的方式:
- Map: 通过键来获取对应的值。
- Collection: 通过迭代器或者增强的 for 循环来遍历集合中的元素。
-
接口层次结构:
-
Map:
Map
接口直接实现类有HashMap
、TreeMap
、LinkedHashMap
等。 -
Collection:
Collection
接口派生出List
、Set
和Queue
等子接口。
-
Map:
-
常见实现类:
-
Map:
HashMap
、TreeMap
、LinkedHashMap
、Hashtable
等。 -
Collection:
ArrayList
、LinkedList
、HashSet
、TreeSet
、PriorityQueue
等。
-
Map:
虽然 Map
和 Collection
在某些方面有相似之处,但它们的设计目的和用途是不同的。Map
主要用于键值对的存储和检索,而 Collection
用于存储和操作一组对象。在实际编码中,根据具体的需求选择合适的接口和实现类。
Collection接口
Collection继承体系图
概述
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
JDK不提供此接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都当成 Object 类型处理;从 JDK 5.0 增加了泛型以后,Java 集合可以记住容器中对象的数据类型。
/*
Collection的工具类:
Collections:
public static void sort(List list)
public static int binarySearch(List<?> list,T key) // 二分查找
public static T max(Collection<?> coll)
public static void reverse(List<?> list)
public static void shuffle(List<?> list)
一般情况下,某个容器的工具类后都会在原先名称上加s
*/
常用方法
1. 添加
add(Object obj)
addAll(Collection coll)
2、获取有效元素的个数
int size()
3、清空集合
void clear()
4、是否是空集合
boolean isEmpty()
5、是否包含某个元素
boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较
6、删除
boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素,只会删除找到的第一个元素
boolean removeAll(Collection coll):取当前集合的差集
7、取两个集合的交集
boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
8、集合是否相等
boolean equals(Object obj)
9、转成对象数组
Object[] toArray()
10、获取集合对象的哈希值
hashCode()
11、迭代器
iterator():返回迭代器对象,用于集合遍历
实例:
Collection 接口提供了一种通用的遍历方式,即通过迭代器(Iterator)进行遍历。下面是使用迭代器进行集合遍历的示例:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class CollectionExample {
public static void main(String[] args) {
// 创建ArrayList实例
Collection<String> collection = new ArrayList<>();
// 向集合中添加元素
collection.add("Apple");
collection.add("Banana");
collection.add("Orange");
collection.add("Grapes");
// 使用迭代器遍历集合
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
}
}
在这个示例中,我们创建了一个 Collection 的实现类 ArrayList,并向集合中添加了一些元素。然后,我们通过调用 iterator() 方法获取了集合的迭代器,使用 while 循环遍历迭代器,逐个获取集合中的元素并输出。
此外,Java 5 引入了增强的 for 循环,也称为 for-each 循环,用于更简洁地遍历集合。以下是使用增强的 for 循环进行集合遍历的示例:
import java.util.ArrayList;
import java.util.Collection;
public class CollectionExample {
public static void main(String[] args) {
// 创建ArrayList实例
Collection<String> collection = new ArrayList<>();
// 向集合中添加元素
collection.add("Apple");
collection.add("Banana");
collection.add("Orange");
collection.add("Grapes");
// 使用增强的 for 循环遍历集合
for (String element : collection) {
System.out.println(element);
}
}
}
在这个示例中,我们直接使用增强的 for 循环遍历集合,简化了代码结构。无论是使用迭代器还是增强的 for 循环,都可以方便地遍历集合中的元素。
List接口
鉴于Java中数组用来存储数据的局限性,我们通常使用List替代数组
List集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。
List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。
JDK API中List接口的实现类常用的有:ArrayList、LinkedList和Vector。
List新增的方法
List除了从Collection集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。
1、添加方法
**void add(int index, Object ele)*在index位置插入ele元素
2、添加全部
**boolean addAll(int index, Collection eles)*从index位置开始将eles中的所有元素添加进来
3、获得元素
**Object get(int index)*获取指定index位置的元素
4、返回元素位置
**int indexOf(Object obj)*返回obj在集合中首次出现的位置
**int lastIndexOf(Object obj)*返回obj在当前集合中末次出现的位置
5、删除
**Object remove(int index)*移除指定index位置的元素,并返回此元素
6、子集合
**Object set(int index, Object ele)*设置指定index位置的元素为e
List(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
7、修改
Object set(int index, Object ele):设置指定index位置的元素为ele
8、迭代器
listIterator(int index)从列表中的指定位置开始,返回列表中的元素(按正确顺序)的列表迭代器。
Set接口
Set接口是Collection的子接口,set接口没有提供额外的方法
Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个Set 集合中,则添加操作失败。
Set 判断两个对象是否相同不是使用 == 运算符,而是根据 equals() 方法
Set接口的常用实现类有HashSet,TreeSet
Queue接口
队列的主要特点是在基本的集合方法之外,还提供特殊的插入、获取和检验操作。每个操作都提供两个方法,一种返回异常,一种返回null或者false.
队列一般满足先进先出规则(FIFO),除了优先队列(priority queue)和栈(stack),但是栈是FILO(先进后出规则),优先队列自己定义了排序规则。
队列不允许插入null元素,但是LinkedList可以。
常见实现类:ArrayQueue,LinkedList
Queue新增的方法
add(E e) 插入一个元素到队列中,失败时返回IllegalStateException (队列容量不够)
element() 返回队列头部的元素
offer(E e) 插入一个元素到队列中,失败时返回false
peek() 返回队列头部的元素,队列为空时返回null
poll() 返回并移除队列的头部元素,队列为空时返回null
remove() 返回并移除队列的头部元素
Map接口
Map接口概述
Map与Collection并列存在。用于保存具有映射关系的数据:key-value
Map 中的 key 和 value 都可以是任何引用类型的数据
Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法
常用String类作为Map的“键”,因为String是不可变的,hash值比较稳定,查询效率高。
**key 和 value 之间存在单向一对一关系,**即通过指定的 key 总能找到value唯一的、确定的 value。
Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类。
Map接口常用方法
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
遍历操作:
Set<K> keySet(): 返回此映射中包含的键的 Set 视图。
Collection<V> values(): 返回此映射中包含的值的 Collection 视图。
Set<Map.Entry<K, V>> entrySet(): 返回此映射中包含的键值对的 Set 视图。
实例:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapExample {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Apple", 5);
map.put("Banana", 2);
map.put("Orange", 8);
map.put("Grapes", 3);
// 基本操作示例
System.out.println("Value for key 'Banana': " + map.get("Banana"));
System.out.println("Contains key 'Orange': " + map.containsKey("Orange"));
System.out.println("Contains value 3: " + map.containsValue(3));
// 遍历操作示例 - 通过键集合
System.out.println("\nIterating over keys:");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key + ": " + map.get(key));
}
// 遍历操作示例 - 通过键值对集合
System.out.println("\nIterating over entries:");
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
for (Map.Entry<String, Integer> entry : entrySet) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 遍历操作示例 - 通过值集合
System.out.println("\nIterating over values:");
for (Integer value : map.values()) {
System.out.println("Value: " + value);
}
}
}
结果:
Value for key 'Banana': 2
Contains key 'Orange': true
Contains value 3: true
Iterating over keys:
Grapes: 3
Banana: 2
Orange: 8
Apple: 5
Iterating over entries:
Grapes: 3
Banana: 2
Orange: 8
Apple: 5
Iterating over values:
Value: 3
Value: 2
Value: 8
Value: 5
注意:请注意,HashMap中的顺序是不确定的,因为HashMap不保证元素的顺序。在实际应用中,如果需要有序的键值对,可以使用LinkedHashMap或TreeMap。
List三大常用集合
ArrayList(重点)
ArrayList 是 List 接口的典型实现类、主要实现类本质上,ArrayList是对象引用的一个”变长”数组。
概述
ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。
ArrayList 继承了 AbstractList ,AbstractList 是List 接口的子类。AbstractList 是个抽象类,适配器设计模式。
ArrayList 是List 接口的子类,此类的定义如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
常用方法
添加元素
ArrayList的add方法进行了重载,可以按照索引值添加,原索引值往后的元素都右移一位
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo01 {
public static void main(String[] args) {
// 实例化List对象,并指定泛型类型
List<String> all = new ArrayList<String>();
// 增加内容,此方法从Collection接口继承而来
all.add("hello ");
// 增加内容,此方法是List接口单独定义的
all.add(0, "LAMP ");
// 增加内容,此方法从Collection接口继承而来
all.add("world");
// 打印all对象调用toString()方法
System.out.println(all);
}
}
结果:[LAMP , hello , world]
以上的操作向集合中增加了三个元素,其中在指定位置增加的操作是List 接口单独定义的。随后进行输出的时候,实际上调用的是toString()方法完成输出的。
可以发现,此时的对象数组并没有长度的限制,长度可以任意长,只要是内存够大就行。
删除元素
删除元素法删除若干个元素,并且使用循环的方式输出。
根据指定位置取的内容的方法,只有List 接口才有定义,其他的任何接口都没有任何的定义。
import java.util.ArrayList;
import java.util.List;
public class ArrayListDemo02 {
public static void main(String[] args) {
List<String> all = new ArrayList<String>(); // 实例化List对象,并指定泛型类型
all.add("hello "); // 增加内容,此方法从Collection接口继承而来
all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的
all.add("world"); // 增加内容,此方法从Collection接口继承而来
all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的
all.remove("world");// 删除指定的对象
System.out.print("集合中的内容是:");
for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来
System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的
}
}
}
结果:
集合中的内容是:LAMP 、
访问元素
import java.util.ArrayList;
public class RunoobTest {
public static void main(String[] args) {
ArrayList<String> sites = new ArrayList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");
System.out.println(sites.get(1)); // 访问第二个元素
}
}
结果:Runoob
修改元素
import java.util.ArrayList;
public class RunoobTest {
public static void main(String[] args) {
ArrayList<String> sites = new ArrayList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");
sites.set(2, "Wiki"); // 第一个参数为索引位置,第二个为要修改的值
System.out.println(sites);
}
}
结果:[Google, Runoob, Wiki, Weibo]
遍历集合
迭代器遍历(两种)
iterator
listIterator
Iterator 接口本身是单向的,不支持倒序遍历。它提供了 hasNext() 方法和 next() 方法,分别用于检查是否有下一个元素并获取下一个元素。因此,你不能直接使用 Iterator 接口来实现倒序遍历。
如果你需要在 Java 中实现倒序遍历,可以考虑使用 ListIterator 接口,尤其是对于 List 类型的集合。ListIterator 提供了 hasPrevious() 方法和 previous() 方法,可以用于在列表中进行倒序遍历。
例子:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
public class IteratorExample {
public static void main(String[] args) {
// 创建一个ArrayList并添加一些元素
List<String> myList = new ArrayList<>();
myList.add("苹果");
myList.add("香蕉");
myList.add("樱桃");
// 使用Iterator遍历列表
System.out.println("使用Iterator遍历列表:");
Iterator<String> iterator = myList.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
// 使用ListIterator遍历和修改列表
System.out.println("\n使用ListIterator遍历和修改列表:");
ListIterator<String> listIterator = myList.listIterator();
while (listIterator.hasNext()) {
String fruit = listIterator.next();
System.out.println(fruit);
// 在遍历过程中添加新元素
if (fruit.equals("香蕉")) {
listIterator.add("葡萄");
}
// 修改当前元素
if (fruit.equals("樱桃")) {
listIterator.set("草莓");
}
}
// 再次使用Iterator遍历修改后的列表
System.out.println("\n再次使用Iterator遍历修改后的列表:");
iterator = myList.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class ListIteratorExample {
public static void main(String[] args) {
// 创建一个ArrayList并添加一些元素
List<String> myList = new ArrayList<>();
myList.add("苹果");
myList.add("香蕉");
myList.add("樱桃");
myList.add("葡萄");
// 使用ListIterator进行正序遍历
System.out.println("正序遍历:");
ListIterator<String> forwardIterator = myList.listIterator();
while (forwardIterator.hasNext()) {
String fruit = forwardIterator.next();
System.out.println(fruit);
}
// 使用ListIterator进行倒序遍历
System.out.println("\n倒序遍历:");
ListIterator<String> backwardIterator = myList.listIterator(myList.size());
while (backwardIterator.hasPrevious()) {
String fruit = backwardIterator.previous();
System.out.println(fruit);
}
}
}
for循环遍历(两种)
for (int i = 0; i < 集合.length; i++)
for(Object o : 集合)//即增强for
Vector
Vector概述
Vector 属于Java 元老级的操作类,是最早的提供了动态对象数组的操作类,在JDK 1.0 的时候就已经推出了此类的使用,只是后来在JDK 1.2 之后引入了Java 类集合框架。但是为了照顾很多已经习惯于使用Vector 的用户,所以在JDK 1.2 之后将Vector 类进行了升级了,让其多实现了一个List 接口,这样才将这个类继续保留了下来
特点
与ArrayList 一样,Vector 本身也属于List 接口的子类,此类的定义如下:
public class Vector<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, Serializable
此类与ArrayList 类一样,都是AbstractList 的子类。所以,此时的操作只要是List 接口的子类就都按照List 进行操作。
Vector 是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的。
在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList;Vector总是比ArrayList慢,所以尽量避免使用。
方法
import java.util.List;
import java.util.Vector;
public class VectorDemo01 {
public static void main(String[] args) {
List<String> all = new Vector<String>(); // 实例化List对象,并指定泛型类型
all.add("hello "); // 增加内容,此方法从Collection接口继承而来
all.add(0, "LAMP ");// 增加内容,此方法是List接口单独定义的
all.add("world"); // 增加内容,此方法从Collection接口继承而来
all.remove(1); // 根据索引删除内容,此方法是List接口单独定义的
all.remove("world");// 删除指定的对象
System.out.print("集合中的内容是:");
for (int x = 0; x < all.size(); x++) { // size()方法从Collection接口继承而来
System.out.print(all.get(x) + "、"); // 此方法是List接口单独定义的
}
}
}
以上的操作结果与使用ArrayList 本身并没有任何的区别。因为操作的时候是以接口为操作的标准
Vector
是 Java 中的一个古老的集合类,它实现了动态数组,类似于 ArrayList
。以下是一些 Vector
类的常用方法:
-
添加元素:
-
addElement(Object obj)
: 将指定的元素添加到此向量的末尾。
-
-
获取元素:
-
elementAt(int index)
: 返回指定索引处的元素。 -
firstElement()
: 返回此向量的第一个组件(即第一个元素)。 -
lastElement()
: 返回此向量的最后一个组件(即最后一个元素)。
-
-
遍历元素:
-
elements()
: 返回此向量的组件的枚举。
-
-
删除元素:
-
removeElement(Object obj)
: 从此向量中移除指定元素的第一个匹配项。 -
removeElementAt(int index)
: 删除指定索引处的元素。
-
-
其他方法:
-
size()
: 返回此向量中的组件数。 -
capacity()
: 返回此向量的当前容量。 -
trimToSize()
: 将此向量的容量修剪为其当前大小。
-
-
与列表接口的方法:
Vector
实现了List
接口,因此还包含了与列表相关的方法,比如add(index, element)
、remove(index)
、indexOf(element)
等。
以下是一个简单的示例,演示了如何使用 Vector
类的一些方法:
javaCopy codeimport java.util.Vector;
import java.util.Enumeration;
public class VectorExample {
public static void main(String[] args) {
// 创建一个Vector
Vector<String> vector = new Vector<>();
// 添加元素
vector.addElement("Apple");
vector.addElement("Banana");
vector.addElement("Cherry");
// 获取元素
System.out.println("Element at index 1: " + vector.elementAt(1));
System.out.println("First element: " + vector.firstElement());
System.out.println("Last element: " + vector.lastElement());
// 遍历元素
System.out.println("\nElements using Enumeration:");
Enumeration<String> enumeration = vector.elements();
while (enumeration.hasMoreElements()) {
System.out.println(enumeration.nextElement());
}
// 删除元素
vector.removeElement("Banana");
System.out.println("\nAfter removing 'Banana': " + vector);
// 获取向量大小和容量
System.out.println("Size: " + vector.size());
System.out.println("Capacity: " + vector.capacity());
// 将容量修剪为当前大小
vector.trimToSize();
System.out.println("After trimming to size: " + vector);
}
}
请注意,Vector
是线程安全的,但由于其同步性能相对较差,通常建议使用 ArrayList
或 LinkedList
等替代品,特别是在不需要线程安全性的情况下。
LinkedList
LinkedList概述
-
此类继承了AbstractList,所以是List 的子类。但是此类也是Queue 接口的子类,Queue 接口定义了如下的方法:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable
LinkedList是双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:
prev变量记录前一个元素的位置
next变量记录下一个元素的位置
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
验证LinkedList子类
import java.util.LinkedList;
import java.util.Queue;
public class TestDemo {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<String>();
queue.add("A");
queue.add("B");
queue.add("C");
//把queue的大小先取出来,否则每循环一次,移除一个元素,就少一个元素,那么queue.size()在变小,就不能循环queue.size()次了。
int len=queue.size();
for (int x = 0; x <len; x++) {
System.out.println(queue.poll());
}
System.out.println(queue);
}
}
Java List小结
1)ArrayList
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
2)Vector
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全,效率低
3)LinkedList
优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高
常见面试题
Vector 类和ArrayList 类的区别
这两个类虽然都是List 接口的子类,但是使用起来有如下的区别:
ArrayList/LinkedList/Vector的异同?谈谈你的理解?ArrayList底层是什么?扩容机制?Vector和ArrayList的最大区别?
ArrayList和LinkedList的异同
二者都线程不安全,相对线程安全的Vector,执行效率高。ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。对于新增和删除操作add(特指插入)和remove,LinkedList比较占优势,因为ArrayList要移动数据。
ArrayList和Vector的区别
Vector和ArrayList几乎是完全相同的,唯一的区别在于Vector是同步类(synchronized),属于强同步类。因此开销就比ArrayList要大,访问要慢。正常情况下,大多数的Java程序员使用ArrayList而不是Vector,因为同步完全可以由程序员自己来控制。
扩容机制
Vector每次扩容请求其大小的2倍空间,而ArrayList是1.5倍。Vector还有一个子类Stack。
Java集合Set
Set扩展Collection接口,无序集合,不允许存放重复的元素。
Set接口常用的实现类有:HashSet、LinkedHashSet、TreeSet
HashSet
特点:
底层其实是包装了一个HashMap实现的
底层数据结构是数组+链表 + 红黑树
具有比较好的读取和查找性能, 可以有null 值
通过equals和HashCode来判断两个元素是否相等
非线程安全
HashSet 集合判断两个元素相等的标准:
两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
对于存放在Set容器中的对象,**对应的类一定要重写equals()**和hashCode(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。
向HashSet中添加元素的过程:
当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
如果两个元素的hashCode()值相等,会再继续调用equals方法,如果equals方法结果为true,添加失败;如果为false,那么会保存该元素,但是该数组的位置已经有元素了,那么会通过链表的方式继续链接。
如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功。
重写 hashCode() 方法的基本原则
在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()
方法的返回值也应相等。
对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
重写 equals() 方法的基本原则
以自定义的Customer类为例,何时需要重写equals()?
当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是
要改写hashCode(),根据一个类的equals方法(改写后),两个截然不
同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,
它们仅仅是两个对象。
因此,违反了“相等的对象必须具有相等的散列码”。
结论:复写equals方法的时候一般都需要同时复写hashCode方法,hashCode的对象的属性也应该参与到equals()中进行计算。
示例:
package com.test.demo14;
import java.util.HashSet;
public class HashSetDemo01 {
public static void main(String[] args) {
HashSet<String> all = new HashSet<>();
all.add("A");
all.add("B");
all.add("C");
all.add("D");
all.add("E");
System.out.println(all);
}
}
结果:
[A, B, C, D, E]
使用HashSet 实例化的Set 接口实例,本身属于无序的存放。
在Collection 接口中定义了将集合变为对象数组,可以用来进行输出。
package com.test.demo14;
import java.util.HashSet;
import java.util.Set;
public class HashSetDemo02 {
public static void main(String[] args) {
Set<String> all = new HashSet<String>(); // 实例化Set接口对象
all.add("A");
all.add("B");
all.add("C");
all.add("D");
all.add("E");
Object obj[]=all.toArray();//将集合变成对象数组
for (Object object : obj) {
System.out.println(object);
}
}
}
结果:
A
B
C
D
E
但是,以上的操作不好,因为在操作的时候已经指定了操作的泛型类型,那么现在最好的做法是由泛型所指定的类型变为指定的数组。
<T> T[] toArray(T[] a)
import java.util.HashSet;
import java.util.Set;
public class HashSetDemo03 {
public static void main(String[] args) {
Set<String> all = new HashSet<String>(); // 实例化Set接口对象
all.add("A");
all.add("B");
all.add("C");
all.add("D");
all.add("E");
String[] str = all.toArray(new String[] {});// 变为指定的泛型类型数组
for (int x = 0; x < str.length; x++) {
System.out.print(str[x] + "、");
}
}
}
下面再进一步验证Set 接口中是不能有重复的内容的。
import java.util.HashSet;
import java.util.Set;
public class HashSetDemo04 {
public static void main(String[] args) {
Set<String> all = new HashSet<String>(); // 实例化Set接口对象
all.add("A");
all.add("A"); // 重复元素
all.add("A"); // 重复元素
all.add("A"); // 重复元素
all.add("A"); // 重复元素
all.add("B");
all.add("C");
all.add("D");
all.add("E");
System.out.println(all);
}
}
以上字符串“A”设置了很多次,因为Set 接口中是不能有任何的重复元素的,所以其最终结果只能有一个“A”。
示例:
要在Customer
类中只根据姓名判断两个对象是否相同,并且在添加到HashSet
时只保留第一个出现的对象,你需要重写equals()
和hashCode()
方法。以下是一个示例:
javaCopy codeimport java.util.HashSet;
class Customer {
private String name;
private String workplace;
public Customer(String name, String workplace) {
this.name = name;
this.workplace = workplace;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Customer customer = (Customer) obj;
return name != null ? name.equals(customer.name) : customer.name == null;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}
public class Main {
public static void main(String[] args) {
HashSet<Customer> customerHashSet = new HashSet<>();
Customer customer1 = new Customer("John", "Company1");
Customer customer2 = new Customer("Jane", "Company2");
Customer customer3 = new Customer("John", "Company3");
// Adding customers to the HashSet
customerHashSet.add(customer1);
customerHashSet.add(customer2);
customerHashSet.add(customer3);
// Displaying the contents of the HashSet
System.out.println("HashSet contents:");
for (Customer customer : customerHashSet) {
System.out.println(customer.getName() + " - " + customer.workplace);
}
}
}
在这个例子中,equals()
方法只比较name
属性,而hashCode()
方法只使用name
属性的哈希码。这样,HashSet
将仅根据姓名判断两个对象是否相同,并且在添加到HashSet
时将保留第一个出现的对象。在输出中,你会看到只有John和Jane被添加到HashSet
,而由于John已经存在,第二个具有相同姓名的对象不会被添加。
LinkedHashSet
特点:
继承HashSet,本质是LinkedHashMap实现
底层数据结构由哈希表(是一个元素为链表的数组)和双向链表组成。
有序的,根据HashCode的值来决定元素的存储位置,同时使用一个链表来维护元素的插入顺序
非线程安全,可以有null 值
TreeSet
特点:
是一种排序的Set集合,实现了SortedSet接口,底层是用TreeMap实现的,本质上是一个红黑树原理
排序分两种:自然排序(存储元素实现Comparable接口)和定制排序(创建TreeSet时,传递一个自己实现的Comparator对象)
正常情况下不能有null值,可以重写Comparable接口 局可以有null值了。
TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。
向 TreeSet 中添加元素时,只有第一个元素无须比较compareTo()方法,后面添加的所有元素都会调用compareTo()方法进行比较。
(上述Set为一个 TreeSet 底层数据结构为一个红黑树
当有一个值添加时,那么该值作为其根节点,当再有一个值添加到树结构中,需要和根节点进行 比较
根节点比较规则为 左节点 < 根节点 < 右节点
左节点 < 根节点 表示为 左和根比较后 其 左值 - 根植 < 0
那么上述 compareTo 代码 中
Student other 表示的就是根值 Student this 表示的就是要添加的值
对于 根植 和 要添加的值 进行比较后的结果就是 树结构中添加数据的依据
并且注意,在比较结果为0时,会对其进行去重操作 ?)
新增的方法如下: (了解)
Comparator comparator()
Object first()
Object last()
Object lower(Object e)
Object higher(Object e)
SortedSet subSet(fromElement, toElement)
SortedSet headSet(toElement)
SortedSet tailSet(fromElement)
下面通过代码来观察其是如何进行排序的。
import java.util.Set;
import java.util.TreeSet;
public class TreeSetDemo01 {
public static void main(String[] args) {
Set<String> all = new TreeSet<String>(); // 实例化Set接口对象\
all.add("D");
all.add("X");
all.add("A");
System.out.println(all);
}
}
结果:[A, D, X]
虽然在增加元素的时候属于无序的操作,但是增加之后却可以为用户进行排序功能的实现。
排序的说明
对于自定义类型Person
如果要进行排序的话,则必须在Person 类中实现Comparable 接口。
package com.test.demo14;
import java.util.Set;
import java.util.TreeSet;
public class TreeSetPersonDemo01 {
public static void main(String[] args) {
Set<Person> all = new TreeSet<Person>();
all.add(new Person("张三", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("赵六", 12));
all.add(new Person("孙七", 13));
System.out.println(all);
}
}
class Person implements Comparable<Person>{
private String name;
private int age;
@Override
public int compareTo(Person o) {
if (this.age>o.age)return 1;
else if (this.age<o.age)
return -1;
else
return 0;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "姓名:" + this.name + ",年龄:" + this.age;
}
}
结果:
[姓名:张三,年龄:10, 姓名:王五,年龄:11, 姓名:赵六,年龄:12, 姓名:孙七,年龄:13]
从以上的结果中可以发现,李四没有了。因为李四的年龄和张三的年龄是一样的,所以会被认为是同一个对象。则此时必须修改Person 类,如果假设年龄相等的话,按字符串进行排序。
public int compareTo(Person per) {
if (this.age > per.age) {
return 1;
} else if (this.age < per.age) {
return -1;
} else {
return this.name.compareTo(per.name);
}
}
此时,可以发现李四出现了,如果加入了同一个人的信息的话,则会认为是重复元素,所以无法继续加入。
关于重复元素的说明(重点)
之前使用Comparable 完成的对于重复元素的判断,那么Set 接口定义的时候本身就是不允许重复元素的,那么证明如果现在真的是有重复元素的话,使用HashSet 也同样可以进行区分。
import java.util.HashSet;
import java.util.Set;
public class HashSetPersonDemo01 {
public static void main(String[] args) {
Set<Person> all = new HashSet<Person>();
all.add(new Person("张三", 10));
all.add(new Person("李四", 10));
all.add(new Person("李四", 10));
all.add(new Person("王五", 11));
all.add(new Person("赵六", 12));
all.add(new Person("孙七", 13));
System.out.println(all);
}
}
此时发现,并没有去掉所谓的重复元素,也就是说之前的操作并不是真正的重复元素的判断,而是通过Comparable接口间接完成的(返回值为0时告诉了程序是相同元素)。
如果要想判断两个对象是否相等,则必须使用Object 类中的equals()方法。
从最正规的来讲,如果要想判断两个对象是否相等,则有两种方法可以完成:
第一种判断两个对象的编码是否一致,这个方法需要通过hashCode()完成,即:每个对象有唯一的编码
还需要进一步验证对象中的每个属性是否相等,需要通过equals()完成。
public boolean equals(Object obj) {
if (this == obj) {
return true;
} if (!(obj instanceof Person)) {
return false;
}
Person per = (Person) obj;
if (per.name.equals(this.name) && per.age == this.age) {
return true;
} else {
return false;
}
}
public int hashCode() {
return this.name.hashCode() * this.age;
}
发现,此时已经不存在重复元素了,所以如果要想去掉重复元素需要依靠hashCode()和equals()方法共同完成
小结:
关于TreeSet 的排序实现,如果是集合中对象是自定义的或者说其他系统定义的类没有实现
Comparable 接口,则不能实现TreeSet 的排序,会报类型转换(转向Comparable 接口)错误。
换句话说要添加到TreeSet 集合中的对象的类型必须实现了Comparable 接口。
不过TreeSet 的集合因为借用了Comparable 接口,同时可以去除重复值,而HashSet 虽然是
Set 接口子类,但是对于没有复写Object 的equals 和hashCode 方法的对象,加入了HashSet
集合中也是不能去掉重复值的。
Java集合Queue
队列是数据结构中比较重要的一种类型,它支持 FIFO,尾部添加、头部删除(先进队列的元素先出队列),跟我们生活中的排队类似。
PriorityQueue
PriorityQueue保存队列元素的顺序并不是按照加入的顺序,而是按照队列元素的大小进行排序的。
PriorityQueue不允许插入null元素。
Deque
Deque接口是Queue接口的子接口,它代表一个双端队列,当程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque。
Java集合Map
Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。
HashMap
概述
HashMap 是Map 的子类,此类的定义如下:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
此类继承了AbstractMap 类,同时可以被克隆,可以被序列化下来。
特点
HashMap是 Map 接口使用频率最高的实现类。
允许使用null键和null值,与HashSet一样,不保证映射的顺序。
所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
一个key-value构成一个entry
所有的entry构成的集合是Set:无序的、不可重复的
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
注意,HashMap中的顺序是不确定的,因为HashMap不保证元素的顺序。在实际应用中,如果需要有序的键值对,可以使用LinkedHashMap
或TreeMap
。
HashMap基本方法
HashMap 是 Java 集合框架中的一种实现,它实现了 Map 接口,允许存储键值对,并提供快速的查找、插入和删除操作。以下是 HashMap 常用的一些方法:
put(K key, V value): 将指定的键值对存储在 HashMap 中。如果键已经存在,则更新对应的值。
get(Object key): 返回指定键映射到的值,如果键不存在,则返回 null。
remove(Object key): 从 HashMap 中移除指定键的映射。
containsKey(Object key): 判断 HashMap 是否包含指定键。
containsValue(Object value): 判断 HashMap 是否包含指定值。
size(): 返回 HashMap 中键值对的数量。
isEmpty(): 判断 HashMap 是否为空,即不包含任何键值对。
clear(): 移除 HashMap 中的所有键值对,使其变为空。
keySet(): 返回一个包含 HashMap 所有键的 Set 集合。
values(): 返回一个包含 HashMap 所有值的 Collection 集合。
entrySet(): 返回一个包含 HashMap 所有键值对的 Set 集合。
putAll(Map<? extends K, ? extends V> m): 将指定映射中的所有键值对复制到 HashMap 中。
replace(K key, V newValue): 替换指定键的值。
replace(K key, V oldValue, V newValue): 当且仅当指定键的值与给定的旧值匹配时,才替换该键的值。
compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction): 使用给定的映射函数计算指定键的值。
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction): 如果指定键的值不存在,则使用给定的映射函数计算该键的值。
computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction): 如果指定键的值存在,则使用给定的映射函数计算该键的新值。
merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction): 将指定键的值与给定值进行合并,如果指定键不存在或其值为 null,则将给定值关联到该键。
下面是一些使用 HashMap
实现的基本操作和遍历操作的示例:
javaCopy codeimport java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapExample {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> map = new HashMap<>();
// 添加键值对
map.put("Apple", 5);
map.put("Banana", 2);
map.put("Orange", 8);
map.put("Grapes", 3);
// 基本操作示例
System.out.println("Value for key 'Banana': " + map.get("Banana"));
System.out.println("Contains key 'Orange': " + map.containsKey("Orange"));
System.out.println("Contains value 3: " + map.containsValue(3));
// 遍历操作示例 - 通过键集合
System.out.println("\nIterating over keys:");
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(key + ": " + map.get(key));
}
// 遍历操作示例 - 通过键值对集合
System.out.println("\nIterating over entries:");
Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
for (Map.Entry<String, Integer> entry : entrySet) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 遍历操作示例 - 通过值集合
System.out.println("\nIterating over values:");
for (Integer value : map.values()) {
System.out.println("Value: " + value);
}
}
}
结果:
Value for key 'Banana': 2
Contains key 'Orange': true
Contains value 3: true
Iterating over keys:
Grapes: 3
Banana: 2
Orange: 8
Apple: 5
Iterating over entries:
Grapes: 3
Banana: 2
Orange: 8
Apple: 5
Iterating over values:
Value: 3
Value: 2
Value: 8
在这个示例中,通过 map.values()
获取值的集合,并通过增强的 for 循环遍历输出了所有的值。这样你就可以看到完整的基本操作和遍历操作的演示。
循环遍历使用remove实例
HashMap
中的 remove
方法通常用于删除指定键对应的映射。然而,正则表达式通常用于匹配字符串的模式,而不是直接应用于键值对的键。如果你想要根据一定的模式删除键值对,你可能需要使用迭代器遍历 HashMap
,然后根据正则表达式匹配来删除符合条件的键值对。
以下是一个使用正则表达式删除符合条件键值对的示例:
javaCopy codeimport java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;
public class HashMapRemoveWithRegex {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple123", 5);
hashMap.put("banana456", 2);
hashMap.put("orange789", 8);
hashMap.put("grapes101", 3);
// 使用正则表达式匹配符合条件的键值对并删除
String regex = ".*\\d+"; // 匹配包含数字的键
Iterator<Map.Entry<String, Integer>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
if (Pattern.matches(regex, key)) {
iterator.remove(); // 使用迭代器的remove方法删除符合条件的键值对
}
}
// 输出删除后的HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个示例中,正则表达式 .*\\d+
匹配包含数字的键。通过迭代器遍历 HashMap
,对每个键应用正则表达式匹配,如果匹配成功,则使用迭代器的 remove
方法删除相应的键值对。请注意,这种方法仅适用于需要正则表达式匹配的情况,如果只是根据精确的键删除,直接使用 remove
方法更为简单。
在这种情况下,使用正则表达式匹配键,然后删除符合条件的键值对。以下是一个例子,使用正则表达式 apple\\d+
匹配键以 “apple” 开头后面跟着数字的情况,然后删除符合条件的键值对:
javaCopy codeimport java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;
public class HashMapRemoveWithRegex {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 12);
hashMap.put("apple12", 13);
hashMap.put("orange", 45);
// 使用正则表达式匹配符合条件的键值对并删除
String regex = "apple\\d+"; // 匹配以 "apple" 开头后面跟着数字的键
Iterator<Map.Entry<String, Integer>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
if (Pattern.matches(regex, key)) {
iterator.remove(); // 使用迭代器的remove方法删除符合条件的键值对
}
}
// 输出删除后的HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,正则表达式 apple\\d+
匹配以 “apple” 开头后面跟着一个或多个数字的键。遍历 HashMap
,对每个键应用正则表达式匹配,如果匹配成功,则使用迭代器的 remove
方法删除相应的键值对。最终输出删除后的 HashMap
。
在 for 循环中直接使用正则表达式删除符合条件的键值对,你可以创建一个新的 HashMap 来保存需要保留的键值对。以下是一个示例:
javaCopy codeimport java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
public class HashMapRemoveWithRegex {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 12);
hashMap.put("apple12", 13);
hashMap.put("orange", 45);
// 使用正则表达式匹配符合条件的键值对并删除
String regex = "apple\\d+"; // 匹配以 "apple" 开头后面跟着数字的键
// 创建新的HashMap保存需要保留的键值对
Map<String, Integer> newHashMap = new HashMap<>();
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
String key = entry.getKey();
if (!Pattern.matches(regex, key)) {
// 如果键不符合正则表达式条件,则添加到新的HashMap中
newHashMap.put(key, entry.getValue());
}
}
// 输出新的HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : newHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,我们遍历原始的 HashMap
,对每个键应用正则表达式匹配,如果不匹配,则将键值对添加到新的 HashMap
中。最后,输出新的 HashMap
,即删除符合条件的键值对后的结果。这种方式避免了直接在循环中修改原始 HashMap
,因为在循环中修改集合可能会导致异常。
**使用 startsWith
方法来检查键是否以特定的前缀开头,**可以直接在循环中使用该方法。以下是使用 startsWith
进行键匹配和删除的示例:
javaCopy codeimport java.util.HashMap;
import java.util.Map;
public class HashMapRemoveWithStartsWith {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 12);
hashMap.put("apple12", 13);
hashMap.put("orange", 45);
// 使用 startsWith 方法判断键匹配并删除
String prefixToRemove = "apple";
Map<String, Integer> newHashMap = new HashMap<>();
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
String key = entry.getKey();
if (!key.startsWith(prefixToRemove)) {
// 如果键不以指定前缀开头,则添加到新的HashMap中
newHashMap.put(key, entry.getValue());
}
}
// 输出新的HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : newHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,我们使用 startsWith
方法来判断键是否以指定的前缀开头。如果键不以指定前缀开头,则将键值对添加到新的 HashMap
中。最后,输出新的 HashMap
,即删除以指定前缀开头的键值对后的结果。
使用 keySet
来简化删除过程。通过 keySet
,你可以直接遍历键的集合,然后使用 startsWith
或其他条件来判断是否需要删除对应的键值对。以下是示例代码:
javaCopy codeimport java.util.HashMap;
import java.util.Map;
public class HashMapRemoveWithKeySet {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 12);
hashMap.put("apple12", 13);
hashMap.put("orange", 45);
// 使用 keySet 删除符合条件的键值对
String prefixToRemove = "apple";
// 创建新的HashMap保存需要保留的键值对
Map<String, Integer> newHashMap = new HashMap<>(hashMap); // 复制原始HashMap
for (String key : hashMap.keySet()) {
if (key.startsWith(prefixToRemove)) {
newHashMap.remove(key);
}
}
// 输出新的HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : newHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,我们首先复制了原始的 HashMap
,然后使用 keySet
遍历键的集合,对每个键进行判断,如果符合条件,则从新的 HashMap
中移除相应的键值对。最后,输出新的 HashMap
,即删除符合条件的键值对后的结果。这种方法可以避免在循环中修改正在遍历的集合。
**在原始的 HashMap
上直接使用 remove
方法进行删除操作。**以下是示例代码:
javaCopy codeimport java.util.HashMap;
import java.util.Map;
public class HashMapRemoveWithDirectRemove {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> hashMap = new HashMap<>();
// 添加键值对
hashMap.put("apple", 12);
hashMap.put("apple12", 13);
hashMap.put("orange", 45);
// 直接在原始HashMap上使用remove删除符合条件的键值对
String prefixToRemove = "apple";
for (String key : new HashMap<>(hashMap).keySet()) {
if (key.startsWith(prefixToRemove)) {
hashMap.remove(key);
}
}
// 输出原始HashMap
System.out.println("HashMap after removal:");
for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,我们使用 hashMap.remove(key)
直接在原始的 HashMap
上删除符合条件的键值对。为了避免在循环中修改集合,我们使用 new HashMap<>(hashMap)
创建了一个原始 HashMap
的副本,然后在副本上进行遍历和删除。这样可以确保在迭代过程中不会直接修改正在遍历的集合。
需要注意的是,在迭代过程中修改集合会导致 ConcurrentModificationException
异常,因此最好在迭代时使用副本或者迭代器的 remove
方法。
为什么循环时不能修改?
在使用迭代器或直接在循环中遍历集合时,如果在遍历的过程中直接修改了集合的结构(例如添加、删除元素),可能会导致 ConcurrentModificationException
异常。
ConcurrentModificationException
是 Java 集合框架中的一种异常,它表示在迭代器遍历集合的过程中,集合的结构被修改了。这样的异常通常是为了防止在多线程环境下的并发修改问题。
例如,在使用 for-each
循环或迭代器遍历集合时,如果在循环内部直接调用 remove
方法或其他修改结构的方法,可能会导致迭代器失效,从而抛出 ConcurrentModificationException
异常。
为了避免这个问题,一种常见的做法是创建集合的副本,然后在副本上进行遍历和修改,而不影响原始集合。或者,可以使用迭代器的 remove
方法来安全地删除元素。
以下是一个简单的示例,演示在循环中直接修改集合结构可能导致的问题:
javaCopy codeimport java.util.ArrayList;
import java.util.List;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<String> myList = new ArrayList<>();
myList.add("Apple");
myList.add("Banana");
myList.add("Orange");
// 使用 for-each 循环遍历集合,并在循环中直接删除元素
for (String fruit : myList) {
if (fruit.equals("Banana")) {
myList.remove(fruit); // 这里会抛出 ConcurrentModificationException 异常
}
}
}
}
上述代码会抛出 ConcurrentModificationException
异常,因为在循环中直接修改了集合的结构。
HashMap的存储结构
- JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
- JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。
HashMap源码中的重要成员属性
常量
ULT_INITIAL_CAPACITY : HashMap的默认容量,16
MAXIMUM_CAPACITY : HashMap的最大支持容量,2^30
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
UNTREEIFY_THRESHOLD:Bucket中红黑树存储的Node小于该默认值,转化为链表
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行。扩容操作这个MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4倍。)
变量
entrySet:存储具体元素的集
size:HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数。
threshold:扩容的临界值,=容量*填充因子
loadFactor:填充因子内部类
Node
在JDK1.8之前是使用的Entry,现在之后用的是Node,两者一个含义
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
}
HashMap的初始化
当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度在哈希表中被称为容量
(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个bucket都有自己的索引,系统可以根据索引快速的查找bucket中的元素。
每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引
用变量,用于指向下一个元素,因此,在一个桶中,就有可能生成一个Entry链。
而且新添加的元素作为链表的head。
HashMap添加元素的过程
通过hash算法计算位置:
向HashMap中添加entry1(key,value),需要首先计算entry1中key的哈希值(根据
key所在类的hashCode()计算得到),此哈希值经过处理以后,得到在底层Entry[]数
组中要存储的位置i。
判断是否可添加:
如果位置i上没有元素,则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3,entry4),则需要通过循环的方法,依次比较entry1中key和其他的entry。如果彼此hash值不同,则直接添加成功。如果hash值不同,继续比较二者是否equals。如果返回值为true,则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后,发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。
HashMap的扩容
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的
长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在
HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算
其在新数组中的位置,并放进去,这就是resize。
HashMap进行扩容的时机:
HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数(size)*填充因子(loadFactor) 时就会进行数组扩容。
1.loadFactor:
loadFactor的默认 值 (DEFAULT_LOAD_FACTOR)为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小(DEFAULT_INITIAL_CAPACITY)为16,那么当HashMap中元素个数超过16x0.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把进行扩容。
2.容量翻倍:
HashMap的扩容是扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
3.树化和反树化
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
总结:
JDK1.8相较于之前的变化:
HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
当首次调用map.put()时,再创建长度为16的数组
数组为Node类型,在jdk7中称为Entry类型
形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
面试题:
负载因子值的大小,对HashMap有什么影响?
负载因子的大小决定了HashMap的数据密度。
负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
TreeMap
TreeMap 子类是允许key 进行排序的操作子类,其本身在操作的时候将按照key 进行排序,另外,key 中的内容可以为任意的对象,但是要求对象所在的类必须实现Comparable 接口。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
特点
-
TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。
-
TreeSet底层使用红黑树结构存储数据
TreeMap 的 Key 的排序:
**自然排序:**TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
**定制排序:**创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。
TreeMap
是 Java 集合框架中的一种实现,它实现了 NavigableMap
接口,是一个有序的键值对集合,基于红黑树实现。以下是 TreeMap
常用的一些方法:
-
put(K key, V value)
: 将指定的键值对存储在TreeMap
中。如果键已经存在,则更新对应的值。 -
get(Object key)
: 返回指定键映射到的值,如果键不存在,则返回null
。 -
remove(Object key)
: 从TreeMap
中移除指定键的映射。 -
containsKey(Object key)
: 判断TreeMap
是否包含指定键。 -
containsValue(Object value)
: 判断TreeMap
是否包含指定值。 -
size()
: 返回TreeMap
中键值对的数量。 -
isEmpty()
: 判断TreeMap
是否为空,即不包含任何键值对。 -
clear()
: 移除TreeMap
中的所有键值对,使其变为空。 -
firstKey()
: 返回TreeMap
中的第一个(最小的)键。 -
lastKey()
: 返回TreeMap
中的最后一个(最大的)键。 -
firstEntry()
: 返回TreeMap
中的第一个键值对。 -
lastEntry()
: 返回TreeMap
中的最后一个键值对。 -
higherKey(K key)
: 返回严格大于给定键的最小键,如果不存在则返回null
。 -
lowerKey(K key)
: 返回严格小于给定键的最大键,如果不存在则返回null
。 -
ceilingKey(K key)
: 返回大于等于给定键的最小键,如果不存在则返回null
。 -
floorKey(K key)
: 返回小于等于给定键的最大键,如果不存在则返回null
。 -
subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)
: 返回键的部分视图,该视图包含所有从fromKey
到toKey
的映射。 -
headMap(K toKey, boolean inclusive)
: 返回严格小于toKey
的部分视图。 -
tailMap(K fromKey, boolean inclusive)
: 返回大于等于fromKey
的部分视图。 -
pollFirstEntry()
: 移除并返回TreeMap
中的第一个键值对。 -
pollLastEntry()
: 移除并返回TreeMap
中的最后一个键值对。 -
descendingKeySet()
: 返回键的逆序视图。 -
descendingMap()
: 返回以逆序比较器的TreeMap
。
实例:
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
// 创建TreeMap实例
TreeMap<Integer, String> treeMap = new TreeMap<>();
// 添加键值对
treeMap.put(3, "Apple");
treeMap.put(1, "Banana");
treeMap.put(4, "Orange");
treeMap.put(2, "Grapes");
// 打印TreeMap中的键值对
System.out.println("TreeMap:");
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 获取第一个和最后一个键值对
System.out.println("\nFirst Entry: " + treeMap.firstEntry());
System.out.println("Last Entry: " + treeMap.lastEntry());
// 获取严格小于3的键值对
System.out.println("\nEntries strictly less than key 3:");
Map<Integer, String> lessThanThree = treeMap.headMap(3);
for (Map.Entry<Integer, String> entry : lessThanThree.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 移除并返回第一个键值对
Map.Entry<Integer, String> firstEntry = treeMap.pollFirstEntry();
System.out.println("\nRemoved First Entry: " + firstEntry);
// 打印修改后的TreeMap
System.out.println("\nTreeMap after removal:");
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
结果:
TreeMap:
1: Banana
2: Grapes
3: Apple
4: Orange
First Entry: 1=Banana
Last Entry: 4=Orange
Entries strictly less than key 3:
1: Banana
2: Grapes
Removed First Entry: 1=Banana
TreeMap after removal:
2: Grapes
3: Apple
4: Orange
将不同公司的学生按照上述排序规则进行排序,可以将公司名称作为键,学生对象作为值。以下是修改后的示例代码:
javaCopy codeimport java.util.Map;
import java.util.TreeMap;
class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public int compareTo(Student other) {
// 先比较年龄,如果年龄相同再比较名字
int ageComparison = Integer.compare(this.age, other.age);
if (ageComparison != 0) {
return ageComparison;
}
return this.name.compareTo(other.name);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class TreeMapCustomSorting {
public static void main(String[] args) {
// 创建TreeMap实例,并根据自定义排序规则排序
TreeMap<String, Student> studentMap = new TreeMap<>();
studentMap.put("CompanyA", new Student("Alice", 22));
studentMap.put("CompanyB", new Student("Bob", 20));
studentMap.put("CompanyC", new Student("Charlie", 25));
studentMap.put("CompanyD", new Student("David", 22));
// 打印排序后的结果
System.out.println("Sorted TreeMap by age and name:");
for (Map.Entry<String, Student> entry : studentMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
在这个例子中,我们将 Student
对象作为值,公司名称作为键,然后使用 TreeMap
来实现按照上述排序规则进行排序。运行程序后,可能的输出结果如下:
plaintextCopy codeSorted TreeMap by age and name:
CompanyB: Student{name='Bob', age=20}
CompanyA: Student{name='Alice', age=22}
CompanyD: Student{name='David', age=22}
CompanyC: Student{name='Charlie', age=25}
这个结果中,不同公司的学生按照年龄升序排序,如果年龄相同则按照名字升序排序。你可以根据实际情况适应这个模式。
Map小结
hashMap和hashTable的区别:
① HashMap可以使用null作为其Key值,而HashTable不可以
② HashMap是线程不安全的,效率高 而HashTable是线程安全的 效率低
③ 在函数使用上没有太大差别
常见数据结构
栈(Stack)是一种常见的数据结构,它遵循后进先出(Last In, First Out,LIFO)的原则。这意味着最后被添加到栈中的元素首先被移除。栈可以看作是一种特殊的线性表,只能在表的一端(称为栈顶)进行插入和删除操作。
以下是栈的一些基本操作和特性:
- 压栈(Push): 将元素添加到栈的顶部。新元素成为栈顶。
- 弹栈(Pop): 从栈的顶部移除元素。栈顶元素被删除,并返回该元素的值。
- 栈顶(Top): 获取栈顶元素的值,但不移除它。
- 判空(Empty): 检查栈是否为空。如果栈中没有元素,就称为空栈。
- 栈的大小: 获取栈中元素的数量。
栈可以通过数组或链表实现。在实际应用中,栈常常用于处理函数调用、表达式求值、括号匹配、浏览器的前进和后退等场景。
下面是一个使用数组实现的简单栈的示例(使用Python语言表示):
pythonCopy codeclass Stack:
def __init__(self):
self.items = []
def is_empty(self):
return len(self.items) == 0
def push(self, item):
self.items.append(item)
def pop(self):
if not self.is_empty():
return self.items.pop()
else:
raise IndexError("pop from an empty stack")
def top(self):
if not self.is_empty():
return self.items[-1]
else:
raise IndexError("top from an empty stack")
def size(self):
return len(self.items)
使用示例:
pythonCopy codestack = Stack()
stack.push(1)
stack.push(2)
stack.push(3)
print("Top:", stack.top()) # 输出:Top: 3
print("Size:", stack.size()) # 输出:Size: 3
popped_item = stack.pop()
print("Popped:", popped_item) # 输出:Popped: 3
print("Is Empty:", stack.is_empty()) # 输出:Is Empty: False
这只是一个简单的示例,实际上,栈还可以通过链表等其他数据结构来实现。在某些编程语言中,也提供了内置的栈数据结构。
队列(Queue)是一种基本的数据结构,它遵循先进先出(First In, First Out,FIFO)的原则。这意味着最先被添加到队列中的元素首先被移除。队列常常用于模拟排队的场景,例如在计算机科学中的任务调度、广度优先搜索等应用中。
以下是队列的一些基本操作和特性:
- 入队(Enqueue): 将元素添加到队列的末尾。新元素成为队列中的最后一个元素。
- 出队(Dequeue): 从队列的头部移除元素。队列头部的元素被删除,并返回该元素的值。
- 队头(Front): 获取队列头部元素的值,但不移除它。
- 队尾(Rear): 获取队列末尾元素的值,但不移除它。
- 判空(Empty): 检查队列是否为空。如果队列中没有元素,就称为空队列。
- 队列大小: 获取队列中元素的数量。
队列可以通过数组或链表等数据结构来实现。在实际应用中,队列的常见实现包括普通队列、双端队列和优先级队列。
下面是一个使用链表实现的简单队列的示例(使用Python语言表示):
pythonCopy codeclass QueueNode:
def __init__(self, data):
self.data = data
self.next = None
class Queue:
def __init__(self):
self.front = None # 队列头部
self.rear = None # 队列尾部
self.size = 0 # 队列大小
def is_empty(self):
return self.size == 0
def enqueue(self, data):
new_node = QueueNode(data)
if self.is_empty():
self.front = self.rear = new_node
else:
self.rear.next = new_node
self.rear = new_node
self.size += 1
def dequeue(self):
if not self.is_empty():
removed_data = self.front.data
self.front = self.front.next
self.size -= 1
if self.is_empty():
self.rear = None
return removed_data
else:
raise IndexError("dequeue from an empty queue")
def front_value(self):
if not self.is_empty():
return self.front.data
else:
raise IndexError("front from an empty queue")
def rear_value(self):
if not self.is_empty():
return self.rear.data
else:
raise IndexError("rear from an empty queue")
def queue_size(self):
return self.size
使用示例:
pythonCopy codequeue = Queue()
queue.enqueue(1)
queue.enqueue(2)
queue.enqueue(3)
print("Front:", queue.front_value()) # 输出:Front: 1
print("Rear:", queue.rear_value()) # 输出:Rear: 3
print("Size:", queue.queue_size()) # 输出:Size: 3
dequeued_item = queue.dequeue()
print("Dequeued:", dequeued_item) # 输出:Dequeued: 1
print("Is Empty:", queue.is_empty()) # 输出:Is Empty: False
这只是一个简单的队列实现示例,实际上,队列还可以通过数组等其他数据结构来实现。在某些编程语言中,也提供了内置的队列数据结构。
数组是一种基本的数据结构,它是由相同类型的元素按一定顺序排列而成的集合。每个元素在数组中都有一个唯一的索引,通过索引可以访问和修改数组中的元素。数组可以是一维的、二维的,甚至是多维的,取决于元素的排列方式。
以下是数组的一些基本概念和特性:
- 索引: 数组中的每个元素都有一个唯一的索引,用于标识元素在数组中的位置。索引通常从零开始,即第一个元素的索引为0,第二个为1,以此类推。
- 元素: 数组中的每个位置存储一个元素,这可以是任何数据类型,例如整数、浮点数、字符等。
- 长度(Length): 数组的长度是指数组中包含的元素个数。长度通常是固定的,一旦数组被创建,其长度通常不能改变。
- 一维数组: 一维数组是最简单的数组形式,元素在一个单一的线性序列中排列。
- 多维数组: 多维数组包含多个维度,可以是二维、三维,甚至更多。例如,二维数组可以看作是一个表格,其中有行和列。
- 动态数组: 有些编程语言支持动态数组,允许在运行时调整数组的大小。
- 静态数组: 静态数组的大小在编译时确定,并在整个生命周期中保持不变。
- 访问时间: 数组的元素可以通过索引直接访问,因此其访问时间是常数时间(O(1))。
下面是一个简单的一维数组的示例(使用Python语言表示):
pythonCopy code# 创建一个一维数组
my_array = [1, 2, 3, 4, 5]
# 访问数组元素
print("Element at index 2:", my_array[2]) # 输出:Element at index 2: 3
# 修改数组元素
my_array[2] = 10
print("Modified array:", my_array) # 输出:Modified array: [1, 2, 10, 4, 5]
# 获取数组长度
print("Array length:", len(my_array)) # 输出:Array length: 5
在实际应用中,数组是一种非常重要且广泛应用的数据结构,用于存储和操作大量的数据。然而,需要注意的是,数组的大小一旦确定,通常不能动态改变,这可能导致一些限制。在一些编程语言中,为了解决这个问题,动态数组(例如Python中的列表)提供了更灵活的选项。
链表(Linked List)是一种基本的数据结构,用于存储一系列元素。与数组不同,链表中的元素在内存中不是顺序存储的,而是通过指针(或引用)相互连接起来的。每个元素被称为节点(Node),节点包含数据和一个指向下一个节点的指针。
链表可以分为单向链表、双向链表和循环链表等不同类型。以下是链表的一些基本概念和特性:
- 节点(Node): 链表中的每个元素被称为节点,节点包含两部分信息:数据和指向下一个节点的指针。
- 头节点(Head): 链表的第一个节点称为头节点,它是整个链表的入口点。
- 尾节点(Tail): 链表的最后一个节点称为尾节点,它的指针通常指向空值(null)或链表的头节点,表示链表的结束。
- 单向链表(Singly Linked List): 每个节点只有一个指针,指向下一个节点。
- 双向链表(Doubly Linked List): 每个节点有两个指针,分别指向前一个节点和后一个节点。
- 循环链表(Circular Linked List): 尾节点的指针指向链表的头节点,形成一个循环。
- 插入操作: 在链表中插入节点,可以在任意位置进行插入,只需要更新相邻节点的指针。
- 删除操作: 从链表中删除节点,同样只需要更新相邻节点的指针。
链表相对于数组的优势在于插入和删除操作的效率较高,因为不需要移动大量元素。然而,链表的缺点是访问元素时需要遍历链表,访问时间复杂度为O(n)。
下面是一个使用Python语言表示的简单单向链表的示例:
pythonCopy codeclass Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None # 头节点
def is_empty(self):
return self.head is None
def append(self, data):
new_node = Node(data)
if self.is_empty():
self.head = new_node
else:
current = self.head
while current.next:
current = current.next
current.next = new_node
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, data):
if self.is_empty():
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next and current.next.data != data:
current = current.next
if current.next:
current.next = current.next.next
def display(self):
current = self.head
while current:
print(current.data, end=" -> ")
current = current.next
print("None")
使用示例:
pythonCopy codelinked_list = LinkedList()
linked_list.append(1)
linked_list.append(2)
linked_list.prepend(0)
linked_list.display() # 输出:0 -> 1 -> 2 -> None
linked_list.delete(1)
linked_list.display() # 输出:0 -> 2 -> None
这只是一个简单的链表实现示例,实际上,链表还可以包含更多的功能,如查找元素、反转链表等。链表是许多其他高级数据结构和算法的基础。
树(Tree)是一种非常重要的数据结构,用于表示层次关系或分层结构。树的结构由节点(Node)和边组成,节点之间的连接关系形成了层次结构。树是一种递归的数据结构,每个节点可以包含其他节点作为其子节点,形成子树。
以下是树的一些基本概念和特性:
- 节点(Node): 树中的基本单元,包含一个数据元素以及指向其子节点的指针。
- 根节点(Root): 树的顶端节点,是整个树的起点。
- 叶子节点(Leaf): 没有子节点的节点称为叶子节点,位于树的末端。
- 父节点(Parent)和子节点(Child): 一个节点的直接上层节点称为其父节点,直接下层节点称为其子节点。
- 兄弟节点(Sibling): 具有相同父节点的节点称为兄弟节点。
- 深度(Depth): 从根节点到某个节点的唯一路径的长度,根节点的深度为0。
- 高度(Height): 从某个节点到其最远叶子节点的路径长度,叶子节点的高度为0。
- 子树(Subtree): 树中的任意节点及其所有后代节点构成的集合称为子树。
- 森林(Forest): 由多棵树组成的集合。
树可以分为多种类型,常见的包括二叉树、二叉搜索树、AVL树、红黑树等。下面简要介绍一些常见的树的类型:
- 二叉树(Binary Tree): 每个节点最多有两个子节点,分别为左子节点和右子节点。
- 二叉搜索树(Binary Search Tree,BST): 一种特殊的二叉树,对于每个节点,其左子树的所有节点值都小于该节点的值,右子树的所有节点值都大于该节点的值。
- AVL树: 一种自平衡的二叉搜索树,确保任意节点的左右子树高度差不超过1。
- 红黑树: 一种自平衡的二叉搜索树,通过引入颜色属性和一些约束条件来保持平衡。
树的应用非常广泛,包括数据库索引结构、文件系统、编译器语法树、图形界面控件的布局等。树结构的特性使得它在解决许多问题时具有高效的性能。
树的遍历是指按照一定的顺序访问树的所有节点。常见的树遍历方式有三种:前序遍历、中序遍历和后序遍历。这些遍历方式在处理树结构时非常重要,它们有助于对树的节点进行有序的访问。
- 前序遍历(Preorder Traversal)
前序遍历的顺序是先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。
plaintextCopy code1. 访问根节点
2. 前序遍历左子树
3. 前序遍历右子树
- 中序遍历(Inorder Traversal)
中序遍历的顺序是先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。
plaintextCopy code1. 中序遍历左子树
2. 访问根节点
3. 中序遍历右子树
中序遍历在二叉搜索树中的应用非常广泛,因为它会按升序访问所有节点,使得遍历结果有序。
- 后序遍历(Postorder Traversal)
后序遍历的顺序是先递归地后序遍历左子树,然后递归地后序遍历右子树,最后访问根节点。
plaintextCopy code1. 后序遍历左子树
2. 后序遍历右子树
3. 访问根节点
后序遍历在处理树的释放和内存管理时比较有用,因为它会先释放子节点的内存,再释放父节点的内存。
遍历的实现
下面是使用Python语言实现的树节点和三种遍历方式的代码:
pythonCopy codeclass TreeNode:
def __init__(self, data):
self.data = data
self.left = None
self.right = None
def preorder_traversal(root):
if root:
print(root.data, end=" ")
preorder_traversal(root.left)
preorder_traversal(root.right)
def inorder_traversal(root):
if root:
inorder_traversal(root.left)
print(root.data, end=" ")
inorder_traversal(root.right)
def postorder_traversal(root):
if root:
postorder_traversal(root.left)
postorder_traversal(root.right)
print(root.data, end=" ")
# 示例树结构
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
# 前序遍历
print("Preorder Traversal:")
preorder_traversal(root)
print()
# 中序遍历
print("Inorder Traversal:")
inorder_traversal(root)
print()
# 后序遍历
print("Postorder Traversal:")
postorder_traversal(root)
以上示例中,树的节点由TreeNode
类表示,然后使用递归方式实现了前序、中序和后序遍历。你可以根据需要调整树的结构和节点的值进行测试。
哈希表(Hash Table)是一种数据结构,通过哈希函数将关键字映射到表中的位置,实现了快速的插入、删除和查找操作。哈希表通常由一个数组和一个哈希函数组成。
以下是哈希表的一些基本概念和特性:
- 哈希函数(Hash Function): 哈希函数接受一个关键字作为输入,返回其对应的哈希值。哈希值通常是一个整数,用于确定关键字在数组中的位置。
- 哈希表数组(Hash Table Array): 哈希表的主要存储结构是一个数组,数组的大小通常是固定的。
- 冲突(Collision): 当两个不同的关键字经过哈希函数得到相同的哈希值时,发生冲突。解决冲突的方法有很多种,常见的包括链地址法和开放地址法。
- 链地址法(Chaining): 将具有相同哈希值的关键字存储在同一个位置的链表中。在发生冲突时,新的关键字被添加到链表的末尾。
- 开放地址法(Open Addressing): 当发生冲突时,尝试在数组中的其他位置找到空槽来存储关键字。开放地址法包括线性探测、二次探测等方法。
- 装载因子(Load Factor): 装载因子是哈希表中已存储的关键字数量与哈希表大小的比率。装载因子的增加可能导致冲突的概率增加,影响哈希表的性能。
- 扩容和缩容: 当装载因子超过某个阈值时,可以选择扩大哈希表的大小,重新计算哈希值并重新分配关键字。相反,当装载因子较小时,可以选择缩小哈希表的大小。
- 不完全哈希函数: 一种导致冲突的哈希函数称为不完全哈希函数。完全哈希函数是一种不会导致冲突的理想情况,但在实践中很难实现。
下面是一个使用链地址法解决冲突的简单哈希表的示例(使用Python语言表示):
pythonCopy codeclass HashTable:
def __init__(self, size):
self.size = size
self.table = [None] * size
def hash_function(self, key):
return hash(key) % self.size
def insert(self, key, value):
index = self.hash_function(key)
if self.table[index] is None:
self.table[index] = [(key, value)]
else:
for i in range(len(self.table[index])):
if self.table[index][i][0] == key:
self.table[index][i] = (key, value) # 更新已存在的键
return
self.table[index].append((key, value))
def get(self, key):
index = self.hash_function(key)
if self.table[index] is not None:
for item in self.table[index]:
if item[0] == key:
return item[1]
raise KeyError("Key not found")
def remove(self, key):
index = self.hash_function(key)
if self.table[index] is not None:
for i in range(len(self.table[index])):
if self.table[index][i][0] == key:
del self.table[index][i]
return
raise KeyError("Key not found")
# 示例使用
hash_table = HashTable(size=10)
hash_table.insert("apple", 5)
hash_table.insert("banana", 8)
hash_table.insert("cherry", 3)
print("Get value for 'banana':", hash_table.get("banana")) # 输出:Get value for 'banana': 8
hash_table.insert("banana", 10) # 更新已存在的键
print("Updated value for 'banana':", hash_table.get("banana")) # 输出:Updated value for 'banana': 10
hash_table.remove("apple")
print("After removing 'apple':", hash_table.get("apple", "Key not found")) # 输出:After removing 'apple': Key not found
这只是一个简单的哈希表实现示例,实际上,哈希表的实现还涉及到很多优化和复杂性问题。在Python中,内置的字典数据结构就是基于哈希表实现的。
图(Graph)是一种抽象数据结构,用于表示多个对象之间的关系。图由节点(顶点)和边组成,边表示节点之间的连接关系。图可以分为有向图和无向图,有权图和无权图等不同类型。
以下是图的一些基本概念和特性:
- 节点(顶点): 图中的基本单元,表示图中的对象。
- 边: 连接两个节点的线段,表示节点之间的关系。边可以有方向,称为有向边,也可以没有方向,称为无向边。
- 有向图和无向图: 如果图的边有方向,称为有向图。如果边没有方向,称为无向图。
- 权重(Weight): 图中的边可以关联一个权重,表示两个节点之间的距离、代价或其他度量。
- 度(Degree): 一个节点的度是指与该节点相连的边的数量。对于有向图,分为入度和出度。
- 路径(Path): 由边连接的节点序列称为路径。
- 环(Cycle): 如果一个路径形成一个闭合的回路,称为环。
- 连通图和非连通图: 如果图中的任意两个节点都存在路径相连,则图是连通的。否则,图是非连通的。
- 子图(Subgraph): 图G的子图是由图G的一部分节点和它们之间的边组成的图。
- 稠密图和稀疏图: 如果图中的边数接近节点数的平方,则称为稠密图。反之,称为稀疏图。
- 邻接矩阵和邻接表: 两种常见的图的表示方式。邻接矩阵使用二维数组表示节点之间的连接关系,邻接表使用链表或数组表示每个节点的邻居。
图的应用非常广泛,包括社交网络关系、路线规划、网络拓扑设计等。以下是一个使用邻接表表示的简单图的示例(使用Python语言表示):
pythonCopy codeclass Graph:
def __init__(self):
self.graph = {}
def add_vertex(self, vertex):
if vertex not in self.graph:
self.graph[vertex] = []
def add_edge(self, vertex1, vertex2):
self.graph[vertex1].append(vertex2)
self.graph[vertex2].append(vertex1)
def display(self):
for vertex in self.graph:
print(f"{vertex}: {', '.join(map(str, self.graph[vertex]))}")
# 示例使用
graph = Graph()
graph.add_vertex(1)
graph.add_vertex(2)
graph.add_vertex(3)
graph.add_vertex(4)
graph.add_edge(1, 2)
graph.add_edge(1, 3)
graph.add_edge(2, 4)
graph.display()
这只是一个简单的图实现示例,实际上,图的操作和算法非常复杂,包括深度优先搜索(DFS)、广度优先搜索(BFS)、最短路径算法(Dijkstra、Bellman-Ford)、最小生成树算法(Prim、Kruskal)等。不同的应用场景可能需要不同的图算法。
**深度优先搜索(Depth-First Search,DFS)和广度优先搜索(Breadth-First Search,BFS)是两种常用的图遍历算法。**它们用于访问和遍历图中的所有节点,但采用不同的顺序。
深度优先搜索(DFS)
深度优先搜索是一种递归的、深度优先的图遍历算法。从图的某个起始节点开始,沿着一条路径尽可能深入,直到不能继续为止,然后回溯并探索其他路径。DFS 常用递归或栈(显式或隐式地使用函数调用栈)来实现。
深度优先搜索的基本步骤:
- 选择一个起始节点。
- 访问该节点,并将其标记为已访问。
- 对于当前节点的每个邻居节点,如果邻居节点未被访问,就递归地对邻居节点进行深度优先搜索。
以下是使用递归实现深度优先搜索的 Python 代码:
pythonCopy codedef dfs(graph, node, visited):
if node not in visited:
print(node, end=" ")
visited.add(node)
for neighbor in graph[node]:
dfs(graph, neighbor, visited)
# 示例使用
graph = {
1: [2, 3],
2: [4, 5],
3: [],
4: [],
5: []
}
visited_set = set()
dfs(graph, 1, visited_set)
广度优先搜索(BFS)
广度优先搜索是一种逐层遍历的图遍历算法。从图的某个起始节点开始,首先访问起始节点,然后按照距离逐层访问其邻居节点。BFS 使用队列来存储待访问的节点。
广度优先搜索的基本步骤:
- 选择一个起始节点。
- 访问该节点,并将其标记为已访问。
- 将该节点的邻居节点加入队列。
- 从队列中取出下一个节点,重复步骤2和3,直到队列为空。
以下是使用队列实现广度优先搜索的 Python 代码:文章来源:https://www.toymoban.com/news/detail-791194.html
pythonCopy codefrom collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(node, end=" ")
visited.add(node)
queue.extend(neighbor for neighbor in graph[node] if neighbor not in visited)
# 示例使用
graph = {
1: [2, 3],
2: [4, 5],
3: [],
4: [],
5: []
}
bfs(graph, 1)
入度和出度。
6. 路径(Path): 由边连接的节点序列称为路径。
7. 环(Cycle): 如果一个路径形成一个闭合的回路,称为环。
8. 连通图和非连通图: 如果图中的任意两个节点都存在路径相连,则图是连通的。否则,图是非连通的。
9. 子图(Subgraph): 图G的子图是由图G的一部分节点和它们之间的边组成的图。
10. 稠密图和稀疏图: 如果图中的边数接近节点数的平方,则称为稠密图。反之,称为稀疏图。
11. 邻接矩阵和邻接表: 两种常见的图的表示方式。邻接矩阵使用二维数组表示节点之间的连接关系,邻接表使用链表或数组表示每个节点的邻居。
图的应用非常广泛,包括社交网络关系、路线规划、网络拓扑设计等。以下是一个使用邻接表表示的简单图的示例(使用Python语言表示):
pythonCopy codeclass Graph:
def __init__(self):
self.graph = {}
def add_vertex(self, vertex):
if vertex not in self.graph:
self.graph[vertex] = []
def add_edge(self, vertex1, vertex2):
self.graph[vertex1].append(vertex2)
self.graph[vertex2].append(vertex1)
def display(self):
for vertex in self.graph:
print(f"{vertex}: {', '.join(map(str, self.graph[vertex]))}")
# 示例使用
graph = Graph()
graph.add_vertex(1)
graph.add_vertex(2)
graph.add_vertex(3)
graph.add_vertex(4)
graph.add_edge(1, 2)
graph.add_edge(1, 3)
graph.add_edge(2, 4)
graph.display()
这只是一个简单的图实现示例,实际上,图的操作和算法非常复杂,包括深度优先搜索(DFS)、广度优先搜索(BFS)、最短路径算法(Dijkstra、Bellman-Ford)、最小生成树算法(Prim、Kruskal)等。不同的应用场景可能需要不同的图算法。
**深度优先搜索(Depth-First Search,DFS)和广度优先搜索(Breadth-First Search,BFS)是两种常用的图遍历算法。**它们用于访问和遍历图中的所有节点,但采用不同的顺序。
深度优先搜索(DFS)
深度优先搜索是一种递归的、深度优先的图遍历算法。从图的某个起始节点开始,沿着一条路径尽可能深入,直到不能继续为止,然后回溯并探索其他路径。DFS 常用递归或栈(显式或隐式地使用函数调用栈)来实现。
深度优先搜索的基本步骤:
- 选择一个起始节点。
- 访问该节点,并将其标记为已访问。
- 对于当前节点的每个邻居节点,如果邻居节点未被访问,就递归地对邻居节点进行深度优先搜索。
以下是使用递归实现深度优先搜索的 Python 代码:
pythonCopy codedef dfs(graph, node, visited):
if node not in visited:
print(node, end=" ")
visited.add(node)
for neighbor in graph[node]:
dfs(graph, neighbor, visited)
# 示例使用
graph = {
1: [2, 3],
2: [4, 5],
3: [],
4: [],
5: []
}
visited_set = set()
dfs(graph, 1, visited_set)
广度优先搜索(BFS)
广度优先搜索是一种逐层遍历的图遍历算法。从图的某个起始节点开始,首先访问起始节点,然后按照距离逐层访问其邻居节点。BFS 使用队列来存储待访问的节点。
广度优先搜索的基本步骤:
- 选择一个起始节点。
- 访问该节点,并将其标记为已访问。
- 将该节点的邻居节点加入队列。
- 从队列中取出下一个节点,重复步骤2和3,直到队列为空。
以下是使用队列实现广度优先搜索的 Python 代码:
pythonCopy codefrom collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(node, end=" ")
visited.add(node)
queue.extend(neighbor for neighbor in graph[node] if neighbor not in visited)
# 示例使用
graph = {
1: [2, 3],
2: [4, 5],
3: [],
4: [],
5: []
}
bfs(graph, 1)
DFS 和 BFS 都有各自的应用场景。DFS 适用于路径查找和连通性问题,而 BFS 适用于最短路径和层次遍历问题。选择使用哪种算法取决于具体的问题需求。文章来源地址https://www.toymoban.com/news/detail-791194.html
到了这里,关于Java集合框架的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!