万字集合概述

本文详细介绍了Java集合框架,包括单列和双列集合、Collection方法、List特性和方法、Set接口及其实现类如HashSet、LinkedHashSet,以及Map接口与HashMap、HashTable的相关操作。内容涵盖集合的添加、删除、查找、遍历、扩容机制和源码分析,旨在深入理解Java集合的使用和原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 集合体系图

下面所有的实现类都重写了toString方法

1.1 单列集合

image-20210318171144320

1.2 双列集合

存放的是K-V

image-20210318171226927

2.Collection方法

因为Collection Set List都是接口不能直接实例化,所以我们选用ArrayList来演示各种方法

2.1 添加操作

image-20210318180219407

2.2 删除操作

image-20210318180255035

2.3 查找

image-20210318173217532

2.4 获取元素个数

list.size()

2.5 判断为空

list.isEmpty()

2.6清空

list.clear();

2.7 遍历

2.7.1 Iterator法

  1. Iterator对象称为迭代器 主要用于遍历Collection集合中的元素

  2. 所有实现了Collection接口的集合类都有一个Iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器

  3. Iterator仅用于遍历集合 其本身不存放对象

  4. 遍历实现步骤

//hasNext()判断是否还有下一个元素
//Next()   1.下移   2.将下移以后的元素返回
//Next() 放回的元素是Object类型
Iterator iterator = list.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
//如过希望再次遍历需要重置 iterator
iterator = list.iterator();
  1. 上述结构的快捷键是itit
  2. 显示所有快捷键的快捷键Ctrl + j

2.7.2 增强for循环

可以替代iterator 特点:增强for循环就是简化版的iterator 底层就是迭代器

可以使用在集合 或者 数组

for (Object a:list) {
    System.out.println(a);
}
//快捷键 I

--------List篇---------

3. List方法

  1. List集合中的元素是有序的(即添加顺序和取出顺序是一致的) 且可以重复
  2. List集合中的每一个元素都有其对应的顺序索引 即支持索引 如 list.get(index)
  3. 主要实现类有 ArrayList LinkedList Vector

3.1 添加操作

相比于Collections方法多了index选项,index为可选选项,表示插入的位置

image-20210704161623550

3.2 删除操作

image-20210318173154421

  1. clear()
  2. retain()

3.3 查找操作

  1. contains 与Collections中的一致

get(index)
  1. indexof(Object obj) 
    //返回obj在集合中首次出现的位置
    
  2. lastIndexof(Object obj) 
    //返回obj在集合中末次出现的位置
    

3.5 赋值(替换)

set(int index,Object o ) //设定指定index位置的元素为o 不能添加元素

3.6 子集合

subList(int fromIndex,int lastIndex)//返回的是左闭右开的

3.7遍历

  1. 迭代器
  2. 增强for
  3. 普通for

4. ArrayList

4.1 性质

  1. 允许放入null
  2. 本质上来说是由数组来实现数据存储的
  3. 基本等同于Vector 除了ArrayList是线程不安全的(执行效率高) 多线程不使用ArrayList

4.1.1 字段

  1. ArrayList中维护的是一个Object类型的数组 transient Object[] elementData
  2. transient 表示该属性不会被序列化

4.1.2 构造器

image-20210318185709752

  1. 如果使用无参构造器 则elemData容量为0 第一次添加 扩容为10 如再次需要扩容 则为原来的1.5倍
  2. 如果使用指定大小的构造器 则初始容量为指定大小 如再次需要扩容 则为原来的1.5

4.2 源码分析

4.2.1 构造

I 无参构造

image-20210319145122814

image-20210319145134468

默认构造一个空数组

II 有参构造

image-20210319170612706

注意如果赋值为0的话 那么数组初始化为EMPTY_ELEMENTDATA 这个和默认构造有区别的

区别就在于以下

image-20210319160432549

则第一次扩容的时候就不是扩为10了 而是扩为1

4.2.2 添加操作

  1. 在添加之前我们要先判断容量是否足够
  2. 而判断的方法就是将所需的最小容量(minCapacity)与现有的容量进行比较,如果不够就进行扩容
  3. 所以我们首先要确定所需的最小容量为多少

image-20210319145256524

ensureCapacityInternal()方法的内部为

image-20210319160348669

这个函数主要有两个目的

  1. 确定最小容量也就是 calculateCapacity()
  2. 判断是否容量足够,不够则进行扩容
I 判断是否扩容
  1. 确定最小容量

通过此方法来确定minCapacity,这里我们传入的minCapacity形式参数一般为size + numNew

image-20210319160432549

  1. 如果elemData是空参构造的数组,且这是第一次添加操作,则会将DEFAULT_CAPACITY(也就是10)与minCapacity(我们传入的是size+1)比较 将较大的作为新的minCapacity。否则就直接将size + numNew作为minCapacity
  2. 如果是通过有参构造的,或者不是第一次添加操作。那么直接将minCapacity返回
  1. 判断是否需要进行扩容

image-20210319165133160

II 扩容操作
  1. 确定新的容量:将minCapacity 与 原空间的1.5倍进行比较 选择较大的那个作为新的 newCapcaity
  2. 之后就是调用copyof来进行扩容

image-20210319165149782

5. Vector

相对于ArrayList他是安全的 但是效率不高 因为做了线程判断

扩容情况

  • 如果是无参 默认为10,满了以后按两倍扩容
  • 如果是有参 则每次按照两倍扩容

源码分析

无参构造

本质上是调用了初始化为10的有参构造

image-20210319173139473

image-20210319173153406

image-20210319173802517

关于capacityIncrement:每次扩充扩多少大小

The amount by which the capacity of the vector is automatically incremented when its size becomes greater than its capacity. If the capacity increment is less than or equal to zero, the capacity of the vector is doubled each time it needs to grow.

添加过程

同样要先判断空间是否足够 若不够 则需要扩容

image-20210319173835646

image-20210319174005832

扩容方法默认为扩容为原来的两倍 若设置了增长量 则按照自己设置的来

image-20210319174112786

6. LinkedList

6.1 特点

  1. 底层实现了双向链表和双端队列特点
  2. 可以添加任意元素(可以重复),包括null
  3. 线程不安全,没有实现同步

  1. 底层维护了双向链表

6.2 底层结构

Node

image-20210704152043225

LinkedList

image-20210704152126038

6.3 add方法

image-20210704155749156

7. List集合的选择

image-20210328162423213

--------Set篇---------

8. Set接口

  1. 无序(添加和取出顺序不一样,但是顺序是固定的) 没有索引

  2. 不允许重复元素,所以最多包含一个null

  3. 和List接口一样,Set接口也是Collection的子接口,因此常用方法和Collection接口一样

  4. 遍历方法

    • 可以使用迭代器
    • 增强for
    • 不能使用索引

9. HashSet

  1. 实现了Set接口

  2. 底层实际上是HashMapimage-20210328164226095 hashMap的底层是数组+链表+红黑树

  3. 不保证元素是有序的 取决于hash

  4. 不能有重复元素,可以存放null值,但是只能有一个

9.1 扩容机制

  1. HashSet底层是HashMap
  2. 添加一个元素时,会将元素的hashCode经过某种运算先计算得到他的hash值 之后根据hash值 转化为对应的 索引
  3. 找到存储表table,看这个索引位置是否已经存放元素
  4. 如果没有 直接加入
  5. 如果有 调用equals 比较,
    1. 如果相同 则放弃添加
    2. 如果不相同 则添加到最后
  6. 在Java8中,如果一条链表的元素个数 > TREEIFY_THRESHOLD(默认是8),并且table的 >= MIN_TREEIFY_CAPACITY(默认是64) 就会进行树化(红黑树)

9.2 第一次add

9.2.1 得到对应的key和value

调用hashSet的add方法

在其中调用map的put 方法,其中有两个参数

  • 作为key的 e
  • 作为value 的PRESENT(作用是占位)
public boolean add(E e) {    return map.put(e, PRESENT)==null;}
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

9.2.2 求出对应的hash值

通过求出元素的hashCode值,之后将hash值无符号右移16位 进行异或运算,得到新的hash值

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}
//不同的类型的hashCode算法是不同的//这里以String为例子public int hashCode() {    int h = hash;// Default to 0    if (h == 0 && value.length > 0) {        char val[] = value;        for (int i = 0; i < value.length; i++) {            h = 31 * h + val[i];        }        hash = h;    }    return h;}

9.2.3 添加操作

Params:

  1. hash – hash for key
  2. key – the key
  3. value – the value to put
  4. onlyIfAbsent – if true, don’t change existing value
  5. evict – if false, the table is in creation mode.
  6. Returns: previous value, or null if none
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node<K,V>[] tab; Node<K,V> p; int n, i;    //如果table为null 或者为空 就先进行扩容    if ((tab = table) == null || (n = tab.length) == 0)        n = (tab = resize()).length;    //如果要加入的索引的位置没有元素 那就直接加入    if ((p = tab[i = (n - 1) & hash]) == null)        tab[i] = newNode(hash, key, value, null);    else {        Node<K,V> e; K k;        //如果当前索引位置对应的链表的第一个元素的hash和key的hash一样        //并且满足下面两个条件之一        //(1)准备加入的key 和 p 指向的Node结点的key是同一个对象        //(2)p 指向的Node结点的key 的equals()和准备加入的key比较后相同        if (p.hash == hash &&            ((k = p.key) == key || (key != null && key.equals(k))))            e = p;        //再判断p 是不是一颗红黑树        //如果是一颗红黑树 就调用putTreeVal,来进行添加        else if (p instanceof TreeNode)            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);        //如果table对应索引位置,已经是一个链表了,就使用for循环比较        //(1)依次和链表的每一个元素比较后,都不相同,则加入到该链表的最后        //		注意再把元素添加到链表以后,立即判断 该链表是否已经到达8个结点 对当前链表就进行树化         //   	再转成红黑树时 如果table的长度小于64 就会进行扩容        //(2)依次和该链表的每一个元素比较后,如果有相同的情况,则break        else {            for (int binCount = 0; ; ++binCount) {                if ((e = p.next) == null) {                    p.next = newNode(hash, key, value, null);                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                        treeifyBin(tab, hash);                    break;                }                if (e.hash == hash &&                    ((k = e.key) == key || (key != null && key.equals(k))))                    break;                p = e;            }        }        if (e != null) { // existing mapping for key            V oldValue = e.value;            if (!onlyIfAbsent || oldValue == null)                e.value = value;            afterNodeAccess(e);            return oldValue;        }    }    ++modCount;    if (++size > threshold)        resize();    afterNodeInsertion(evict);    return null;}

扩容操作

扩容操作即调用resize函数。

触发扩容操作的有以下情况

  1. 如果原来的table为null或者空表
if ((tab = table) == null || (n = tab.length) == 0)    n = (tab = resize()).length;
  1. 如果size大于临界值
if (++size > threshold)    resize();

扩容的函数如下:

我们要得到新的

  • table
  • threshold
final Node<K,V>[] resize() {    Node<K,V>[] oldTab = table;    int oldCap = (oldTab == null) ? 0 : oldTab.length;    int oldThr = threshold;    int newCap, newThr = 0;    if (oldCap > 0) {        if (oldCap >= MAXIMUM_CAPACITY) {            threshold = Integer.MAX_VALUE;            return oldTab;        }        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                 oldCap >= DEFAULT_INITIAL_CAPACITY)            newThr = oldThr << 1; // double threshold    }    else if (oldThr > 0) // initial capacity was placed in threshold        newCap = oldThr;    else {               // zero initial threshold signifies using defaults        newCap = DEFAULT_INITIAL_CAPACITY;        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);    }    if (newThr == 0) {        float ft = (float)newCap * loadFactor;        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                  (int)ft : Integer.MAX_VALUE);    }    threshold = newThr;    @SuppressWarnings({"rawtypes","unchecked"})    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];    table = newTab;    if (oldTab != null) {        for (int j = 0; j < oldCap; ++j) {            Node<K,V> e;            if ((e = oldTab[j]) != null) {                oldTab[j] = null;                if (e.next == null)                    newTab[e.hash & (newCap - 1)] = e;                else if (e instanceof TreeNode)                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                else { // preserve order                    Node<K,V> loHead = null, loTail = null;                    Node<K,V> hiHead = null, hiTail = null;                    Node<K,V> next;                    do {                        next = e.next;                        if ((e.hash & oldCap) == 0) {                            if (loTail == null)                                loHead = e;                            else                                loTail.next = e;                            loTail = e;                        }                        else {                            if (hiTail == null)                                hiHead = e;                            else                                hiTail.next = e;                            hiTail = e;                        }                    } while ((e = next) != null);                    if (loTail != null) {                        loTail.next = null;                        newTab[j] = loHead;                    }                    if (hiTail != null) {                        hiTail.next = null;                        newTab[j + oldCap] = hiHead;                    }                }            }        }    }    return newTab;}

10. LinkedHashSet

  1. LinkedHashSet是HashSet的子类
  2. 底层是一个LinkedHashMap,底层维护了一个 数组 +双向链表
  3. LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
  4. LinkedHashSet不允许添加重复元素

image-20210329194943917

  1. LinkedHashSet 加入顺序和取出顺序是一致的
  2. LinkedHashSet 底层维护的是一个LinkedHashMap
  3. LinkedHashMap 底层结构是 数组table +双向链表
  4. 添加第一次时, 直接将table扩容为16 存放的结点类型是LinkedHashMap$Entry
  5. 数组是 HashMapNode[]存放的元素是LinkedHashMapNode[] 存放的元素是 LinkedHashMapNode[]LinkedHashMapEntry

image-20210705111326896

static class Entry<K,V> extends HashMap.Node<K,V> {    Entry<K,V> before, after;    Entry(int hash, K key, V value, Node<K,V> next) {        super(hash, key, value, next);    }}

-------Map篇----------

11. Map接口

11.1 特点

  1. Map与Collection并列存在。用于保存具有映射关系的数据 key-value
  2. Map中的key和value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
  3. Map中的key不允许重复,原因和HashSet一样
  4. Map中的value可以重复
  5. Map中的key可以为null,value也可以为null
  6. 常用String作为key
  7. key和value之间存在单一一对一关系,即通过指定的key总能找到对应的value

  1. k-v 最后是 HashMap$Node node= new newNode(hash,key,value,null)

  2. k-v为了方便程序员遍历 还会创建 EntrySet集合 该集合存放的元素类型Entry ,而一个Entry 对象就有k,v EntrySet<Entry<k,v>> 即transinet Set<Map.Entry<K,V>> entrySet;

  3. entrySet中,定义的类型是Map.Entry 但是实际上存放的还是HashMap$Node

    这是因为 static class Node<K,V> implements Map.Entry<K,V>

  4. Map.Entry 提供了重要方法 getKey() getValue()

  5. 得到所有key keySet()

  6. 得到所有values 用values()

11.2 方法

增加(修改)

 public V put(K key, V value)
public void putAll(Map<? extends K, ? extends V> m)

删除

public V remove(Object key)
default boolean remove(Object key, Object value) 
public void clear()

查找

public V get(Object key)
public boolean containsKey(Object key)	
public boolean containsValue(Object value)
public V getOrDefault(Object key, V defaultValue) {    Node<K,V> e;    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;}

遍历

public Set<K> keySet()
public Collection<V> values() 
public Set<Map.Entry<K,V>> entrySet();//其中Entry的方法有1. getKey();2. V getValue();3. V setValue(V value);4. boolean equals(Object o);

image-20210706102451053

image-20210706102952925

杂项

 public int size()
public boolean isEmpty()
  1. put 添加
  2. remove 根据键删除
  3. get 根据键获取值
  4. containsKey 是否存在键
  5. size
  6. isEmpty
  7. clear

11.3 遍历

  1. containsKey

  2. keySet 获取所有的键

  3. entrySet 获取所有的关系k-v

  4. values 获取所有的值

12. HashMap

12.1 特点

image-20210706113612395

12.2 底层源码

13. HashTable

1)存放的元素是键值对:即K-V

  1. hashtable的键和值都不能为null,否则会抛出NullPointerException

  2. hashtable的键和值都不能为null,否则会抛出NullPointerException

  3. hashTable是线程安全的(synchronized), hashMap 是线程不安全的

14. Properties

15. TreeSet

16. TreeMap

17. Collections工具栏

image-20210706145507622

image-20210706151239346

16. 集合选择

image-20210706115830437

<think>好的,我需要帮助用户理解PPO算法的详细内容、实现方式以及源码解析。首先,用户提到了参考中的两篇资料,其中一篇是通俗讲解RLHF和PPO实现的,另一篇是结合Python代码的PPO示例。我需要结合这些资料的信息,同时遵循用户给出的格式要求。 首先,我应该明确PPO的基本概念。PPO(Proximal Policy Optimization)是强化学习中的一种策略优化方法,属于策略梯度算法的一种改进。它的核心目标是提高训练的稳定性和样本效率,通过限制策略更新的幅度来避免训练过程中的剧烈波动。 接下来,用户要求详细介绍实现方式。根据参考中的资料,PPO有两种主要形式:PPO-Clip和PPO-Penalty。其中,PPO-Clip更常用,它通过引入一个剪切函数来限制新旧策略之间的差异,确保更新后的策略不会偏离旧策略太远。而PPO-Penalty则通过KL散度惩罚项来实现类似的限制。 然后,源码解析部分需要结合具体的代码示例。用户提供的参考中有使用PyTorch和Gym环境的代码示例,我需要解释代码中的关键部分,比如策略网络的结构、经验收集、优势估计、损失函数计算等。例如,在策略网络中,通常会使用Actor-Critic结构,Actor负责生成动作的概率分布,Critic评估状态的价值。 在实现步骤部分,我需要分步骤说明PPO的工作流程。首先是经验收集,使用当前策略与环境交互,收集状态、动作、奖励等数据。接着是优势估计,通常使用广义优势估计(GAE)来计算每个时间步的优势值。然后计算重要性采样比率,用于调整旧策略和新策略之间的差异。之后是损失函数的构建,包括策略损失、价值函数损失和熵正则项。最后进行梯度更新,并多次执行这些步骤直到收敛。 在代码解析中,我需要指出关键部分,比如剪切函数的应用,优势值的归一化处理,以及损失函数的具体实现。例如,在计算策略损失时,新旧策略的概率比会被剪切在1±ε的范围内,以防止过大的更新。 最后,根据用户的要求,生成相关问题。这些问题应该围绕PPO的改进方法、参数调整、与其他算法的比较以及实际应用中的挑战等方面展开。 需要确保整个回答符合格式要求:行内数学公式用$...$,独立公式用$$...$$,使用中文,结构清晰,引用标识自然添加。同时,避免使用过多的专业术语,保持通俗易懂,让用户能够逐步理解PPO的实现细节。</think>### PPO算法详解与实现解析 #### 一、算法基础 PPO(Proximal Policy Optimization)是一种基于策略梯度的强化学习算法,核心思想是通过约束策略更新的幅度来保证训练稳定性。其核心公式为: $$L^{CLIP}(\theta) = \mathbb{E}_t \left[\min\left( r_t(\theta)A_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon)A_t \right)\right]$$ 其中$r_t(\theta)=\frac{\pi_\theta(a_t|s_t)}{\pi_{\theta_{old}}(a_t|s_t)}$是重要性采样比率,$\epsilon$是剪切系数(通常取0.1-0.3)[^1]。 #### 二、实现步骤 1. **经验收集**:使用当前策略$\pi_{\theta_{old}}$与环境交互,收集轨迹数据$(s_t,a_t,r_t,s_{t+1})$ 2. **优势估计**:计算广义优势估计(GAE): $$A_t = \delta_t + (\gamma\lambda)\delta_{t+1} + \cdots + (\gamma\lambda)^{T-t+1}\delta_{T-1}$$ 其中$\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)$ 3. **重要性采样**:计算新旧策略概率比$r_t(\theta)$ 4. **损失计算**: - 策略损失:$L^{CLIP}$ - 价值损失:$(V_\theta(s_t) - R_t)^2$ - 熵正则项:$-\beta H(\pi_\theta(\cdot|s_t))$ 5. **梯度更新**:使用Adam优化器联合优化总损失$L_{total} = L^{CLIP} + c_1 L^{VF} + c_2 L^{Entropy}$ #### 三、代码解析(基于PyTorch) ```python import torch import torch.nn as nn from torch.optim import Adam class ActorCritic(nn.Module): def __init__(self, obs_dim, act_dim): super().__init__() self.actor = nn.Sequential( # 策略网络 nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, act_dim)) self.critic = nn.Sequential( # 价值网络 nn.Linear(obs_dim, 64), nn.Tanh(), nn.Linear(64, 1)) def forward(self, x): return torch.distributions.Categorical(logits=self.actor(x)), self.critic(x) ``` #### 四、训练流程关键代码 ```python def update(self, obs, acts, advs, returns, old_log_probs): # 计算新策略的概率分布 dist, values = self.ac(obs) new_log_probs = dist.log_prob(acts) # 重要性采样比率 ratio = (new_log_probs - old_log_probs).exp() # 剪切策略损失 surr1 = ratio * advs surr2 = torch.clamp(ratio, 1.0 - self.eps, 1.0 + self.eps) * advs policy_loss = -torch.min(surr1, surr2).mean() # 价值损失 value_loss = 0.5 * (returns - values).pow(2).mean() # 熵正则项 entropy_loss = -dist.entropy().mean() # 总损失 loss = policy_loss + 0.5*value_loss + 0.01*entropy_loss # 梯度更新 self.optimizer.zero_grad() loss.backward() self.optimizer.step() ``` 关键参数说明: - `advs`:经过归一化的优势估计值 - `returns`:实际累积回报 - `self.eps`:剪切系数(默认0.2) - 优化器通常设置学习率3e-4[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值