hash是把输入的任意对象通过哈希算法变换成固定长度的输出,该输出就是哈希值。不同的输入可能会哈希相同的输出,所以不可能从哈希值来确定唯一的输入值,但可以键哈希值作为这个对象的一个特征
HashMap内部旧采用了哈希算法来存储元素。但由于哈希算法对于不同的输入可能会哈希成相同的输出,而且数组空间不可能是无限大的,所以在同个位置上就不可避免的需要存储多个元素了,这种情况就叫做哈希冲突。此外,HashMap不保证元素的存储顺序和迭代顺序。因为根据需要HashMap会对元素重新哈希,元素的顺序也会被再次打乱,因此在不同时间段其存储和迭代顺序都可能会发生变化。此外,HashMap也不保证线程安全,如果有多个线程同时进行写操作的化可能会导致数据错乱甚至线程死锁。
HashMap 中的全局常量主要看以下几个
//哈希桶数组的默认容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//哈希桶数组能够达到的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//为了提高效率,当链表的长度超出这个值时,就将链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树的长度小于这个值时,就将红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;
装载因子用于规定数组在自动扩容之前数据占有其容量的最高比例,即当数据量占有数组的容量达到这个比例后,数组将自动扩容。装载因子衡量的是一个散列表的空间的使用程度,装载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表的散列表来说,查找一个元素的平均时间是O(1+a),因此装载因子越大,对空间的利用程度就越高,相对应的是查找效率越低。如果装载因子太小,那么数组的数据将过于稀疏,对空间的利用率就变低,相应查找效率也会提升
官方默认的装载因子大小是 DEFAULT_LOAD_FACTOR,即 0.75,是平衡空间利用率和查找效率两者之后的结果。在实际情况中,如果内存空间较多而对时间效率要求很高,可以选择降低装载因子大小;如果内存空间紧张而对时间效率要求不高,则可以选择加大装载因子
此外,即使装载因子和哈希算法设计得再合理,也难免会出现由于哈希冲突导致链表长度过长的情况,这也将影响 HashMap 的性能。为了优化性能,从 JDK1.8 开始引入了红黑树,当链表长度超出 TREEIFY_THRESHOLD 规定的值时,链表就会被转换为红黑树,利用红黑树快速增删改查的特点以提高 HashMap 的性能
//哈希桶数组,在第一次使用时才初始化
//容量值应是2的整数倍
transient Node<K, V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K, V>> entrySet;
//Map的大小
transient int size;
//每当Map的结构发生变化时,此参数就会递增
//当在对Map进行迭代操作时,迭代器会检查此参数值
//如果检查到此参数的值发生变化,就说明在迭代的过程中Map的结构发生了变化,因此会直接抛出异常
transient int modCount;
//数组的扩容临界点,当数组的数据量达到这个值时就会进行扩容操作
//计算方法:当前容量 x 装载因子
int threshold;
//使用的装载因子值
final float loadFactor;
如果使用空参构造创建HashMap对象,在底层把DEFAULT_LOAD_FACTOR赋值给loadFactor 也就是把默认的加载因子0.75赋值给成员变量loadFactor,也就是表示当前HashMap的加载因子就是0.75。在这个时候底层的数组还没有创建。
//设置Map的初始化大小和装载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//设置初始化大小
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//传入初始数据
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
在上边说过,HashMap 是 数组+链表+红黑树 的结合体,数组中每一项元素的类型分为四种可能:null、单独一个结点、链表、红黑树
每一个要插入的键值对都会被包装为 Node 对象,根据 key 的哈希值来决定 Node 对象在数组中的位置。如果计算出的位置此时不包含值则直接将 Node 对象放到该位置即可;如果包含值则说明发生了哈希碰撞,此时就需要将 Node 对象插入到链表或者是红黑树中。如果 key 与链表或红黑树中某个已有结点的 key 相等(hash 值相等且两者 equals 成立),则新添加的 Node 对象将覆盖原有数据
当哈希算法的计算结果越分散均匀,发生哈希碰撞的概率就越小,HashMap 的存取效率就会越高
Node 类的声明如下所示
static class Node<K,V> implements Map.Entry<K,V> {
//key 的哈希值
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;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
public final String toString() {
return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
插入键值对的方法是 put(K key,V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//计算 key 的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
putVal
方法比较复杂,需要考虑以下几种情况:
putVal()
方法判断 如果数组为空 或者 数组长度等于0 说明是第一次添加 将数组扩容为16 此时threshold为12 然后直接将数据存储到tab中 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //定义一个局部变量,用来记录哈希表中数组的地址值。
Node<K,V> p; //临时的第三方变量
int n, i; // n 当前数组的长度 i 索引
//判断如果数组为空 或者 数组长度等于0 说明是第一次添加 将数组扩容
//如果是第一次扩容,底层会创建一个数组长度为16, threshold 也是就阈值为 16 *0.75=12
newCap = DEFAULT_INITIAL_CAPACITY 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//阈值, 16 * 0.75 = 12
//如果不受第一次添加元素,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把原来的数据全部转移到新的哈希表中
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// i = (n - 1) & hash 拿着数组的长度跟键的hash值进行计算,计算出当前键值对对象,在数组中应存入的位置
// p = tab[i] 获取数组中对应元素的数据
// 如果是第一次添加数据,获取出来的是null,通过newNode方法创建一个新的Node对象 直接放在数组中
// 如果是第二次添加,计算出来数组应存入的索引处已有元素 也就是判断不为空了,将会走else方法
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//p.hash == hash
//p.hash 数组中键值对的hash值
//hash 当前要添加键值对的哈希值
//如果键不一样,此时返回false
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断数组中获取出来的键值对是不是红黑树中的节点
//如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果从数组种获取出来的键值对不受红黑树中的节点,表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
//判断目标节点的next处是否为空, 也就是将当前要添加的数组 添加到 目标节点的next处
if ((e = p.next) == null) {
//此时会创建一个新的节点,挂在下面形成链表
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否超过8,如果超过,就会调用方法treeifyBin
//rteeifyBin方法的底层还会继续判断
//判断数组的长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果哈希值一样,就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e 为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//新值覆盖旧值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果当前数组长度大于加载因子,则进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//当前没有覆盖任何元素,返回null
return null;
}
获取 value 对应的是 get(Object key)方法
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
//根据 key 获取结点
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
//只有当 table 不为空且 hash 对应的位置不为 null 时说明才有可能存在该 key
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
//如果与头结点相等的话说明找到了对应值
return first;
// e != null 说明存在该位置存在链表或红黑树,那么就从这两者中获取
if ((e = first.next) != null) {
if (first instanceof TreeNode) //红黑树
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
do {
//链表
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
从 Map 中移除键值对的操作,对于其底层数据结构的体现就是要移除对某个 Node 对象的引用,这个数据结构可能是数组、红黑树、或者链表
//如果真的存在该 key,则返回对应的 value,否则返回 null
public V remove(Object key) {
Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* @param value key对应的值,只有当matchValue为true时才需要使用到,否则忽略该值
* @param matchValue 如果为 true ,则只有当找到key和value均匹配的结点时才会移除该结点,否则只要key相等就直接移除该元素
* @param movable if false do not move other nodes while removing
* @return the node, or null if none
*/
final Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K, V>[] tab;
Node<K, V> p;
int n, index;
//只有当 table 不为空且 hash 对应的位置不为 null 时说明才有可能存在该 key
if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
//如果与头结点 p 的 key 相等,那么就已经找到了目标 node
node = p;
else if ((e = p.next) != null) {
//存在红黑树或者链表
if (p instanceof TreeNode) //红黑树
node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
else {
//链表
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//node != null 说明存在 key 对应结点
//如果 matchValue 为 false ,则此处就可以直接移除结点 node
//如果 matchValue 为 true ,则当 value 相等时才需要移除该结点
if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
if (node instanceof TreeNode) //红黑树
((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
else if (node == p) //对应 key 与头结点相等的情况,此时直接将指针移向下一位即可
tab[index] = node.next;
else //链表
p.next = node.next;
++modCount;
--size;
//用于 LinkedHashMap ,在 HashMap 中是空实现
afterNodeRemoval(node);
return node;
}
}
return null;
}
当 HashMap 中的元素个数超出 threshold 时(数组容量 与 loadFactor 的乘积),就会进行数组扩容。例如,假设数组当前大小是 16,loadFactor 值是 0.75,那么当 HashMap 中的元素个数达到 12 个时,就会自动触发扩容操作,把数组的大小扩充到 2 * 16 = 32,即扩大一倍,然后重新计算每个元素在新数组中的位置,这是一个非常消耗性能的操作,所以如果已经预知到待存入 HashMap 的数据量,那么在初始化 HashMap 时直接指定初始化大小会是一种更为高效的做法
默认情况下,哈希数组的容量是 16,loadFactor 是 0.75,这是平衡空间利用率和时间效率两者之后的结果
初始化数组和扩容数组这两个操作对应的是 resize()
方法
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) {
//oldCap > 0 对应的是 table 已被初始化的情况,此时是来判断是否需要进行扩容
//如果数组已达到最大容量,则不再进行扩容,并将扩容临界点 threshold 提升到 Integer.MAX_VALUE,结束
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
//如果将数组的现有容量提升到两倍依然小于 MAXIMUM_CAPACITY,且现有容量大于等于 DEFAULT_INITIAL_CAPACITY
//则将数组的容量和扩容临界值均提升为原先的两倍
newThr = oldThr << 1;
}
//此处应该还有一种情况
//即将数组的现有容量提升到现在的两倍后大于等于 MAXIMUM_CAPACITY 的情况
//此时 newThr 等于 0,newCap 等于 oldCap 的两倍值
//此处并没有对 newCap 的数值进行还原,说明 HashMap 是允许扩容后容量超出 MAXIMUM_CAPACITY 的
//只是在现有容量超出 MAXIMUM_CAPACITY 后,不允许再次进行扩容
} else if (oldThr > 0) {
//oldCap <= 0 && oldThr > 0
//对应的是 table 还未被初始化,且在调用构造函数时有传入 initialCapacity 或者 Map 的情况
//此时就直接将容量提升为 threshold,在后边重新计算新的扩容临界值
newCap = oldThr;
} else {
//oldCap <= 0 && oldThr <= 0
//对应的是 table 还未被初始化,且调用的是无参构造函数
//将 table 的容量扩充到默认大小,并使用默认的装载因子来计算扩容临界值
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;
//e.next == null 说明元素 e 没有产生 hash 冲突,因此可以直接转移该元素
if (e.next == null)
//计算元素 e 在新数组中的位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) //存在哈希冲突且是用了红黑树
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {
//存在哈希冲突且是用了链表
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;
}
和HashMap类似,但它是线程安全的,它的内部方法基本都使用了synchronized修饰。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁
LinkedHashMap是HashMap的直接子类,两者的唯一区别是LinkedHashMap在HashMap的基础上,采用双向链表的形式将所有entry链接起来,这样是为了保证元素的迭代顺序跟插入顺序相同
HashMap 中每个存入的键值对都会被包装为 Node 对象,LinkedHashMap 则是包装为 Entry 对象,看 newNode 方法就知道了。Entry 类在 Node 类的基础上扩展了两个新的成员变量:before 和 after,这两个变量就是 LinkedHashMap 来实现有序访问的关键。每当保存了新的键值对,Entry 就会通过这两个变量将其和之前的键值对串联起来,保存为链表的尾结点,从而保留了键值对的顺序信息
TreeMap跟TreeSet底层原理一样,都是红黑树结构的,增删改查性能较好。不能添加重复的键,可以根据键进行排序,有索引。
默认按照键的从小到大进行排序,也可以自己规定键的排序规则。
两种排序规则:
在TreeMap中,每一个元素其实就是一个Entry对象
private final Comparator<? super K> comparator; 比较规则
private transient Entry<K,V> root; 记录根节点的地址值
private transient int size = 0; 表示集合的长度,也表示红黑树当中节点的个数
K key; 键
V value; 值
Entry<K,V> left; 记录左子节点的地址值
Entry<K,V> right; 记录右子节点的地址值
Entry<K,V> parent; 记录父节点的地址值
boolean color = BLACK; 记录节点的颜色 用的布尔值表示
public TreeMap() {
comparator = null;
}
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
它是HashMap的一个线程安全的、支持高效并发的版本
无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap 可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作
文章浏览阅读8.1k次。2016.12.20 乐刻运动 体育运动 B轮 亿元及以上人民币2016.12.14 SenseTime商汤科技 企业服务 B轮 1.2亿美元2016.12.13 OL里昂 体育运动 战略投资 1亿欧元2016.12.13 High Fidelity _idg 被投企业清单
文章浏览阅读127次。此外,VR全景看车还可以提供个性化选车服务,根据消费者的喜好和需求来匹配合适的车型,消费者自定义汽车的外观和配置,以此来挑选更符合心意的车辆。消费者通过VR全景技术,身临其境云端看车,720度多角度缩放查看,同传统的图文视频的看车模式相比,VR看车展现的更加详细,3D可视化说明书、热点标注、一键更换外观等,帮助消费者解决了不少的看车难题。除此之外,在VR虚拟车展中,我们还可以适当的添加一些营销活动,例如签到有礼、分享转发、砸金蛋等趣味化游戏,让消费者得到一些购车优惠,这样能更有效提升意向客户的购买率。_vr看车
文章浏览阅读460次,点赞5次,收藏8次。3 axios 第三方ajax,只有ajax,没有别的,小--》底层还是基于XMLHttpRequest。提供了一个 JavaScript 接口,用于访问和操纵 HTTP 管道的一些具体部分。# 1 使用jq的ajax ===》不好---》引入了jq框架,好多功能用不到。number:数字开头,只保留数字,后面的字母不保留;lazy:等待input框的数据绑定时区焦点之后再变化。# 2 原生js fetch。trim:去除首位的空格。_input v-modal原生并实现lazy
文章浏览阅读158次。设置控件,这里用rVar,gVar,bVar来储存rgb数值,用Scale制作滑块。_tkinter 颜色16进制代码大全
文章浏览阅读185次。【代码】全角半角互相转换。_r如何切换全角半角
文章浏览阅读178次。研发管理(Research and Development Management,R&D Management)是一种系统性的管理活动,是以产品开发流程为基础的项目管理体系,旨在规划、组织、协调和监督研发项目,对研发项目的人员、计划、质量、成本等进行综合管理,从而打造高效能的研发团队,更好更快地实现项目目标。研发管理的本质是从流程化,标准化,制度化等维度建立管理机制。最终的核心目标是通过管理的法治建立标准化的操作规范,再通过标准化的规范提升人员的协作效率、监督机制、系统稳定性/安全性等。
文章浏览阅读5k次。https://github.com/guanzhi/GmSSL/blob/master/include/openssl/ssl.h#L1673-L1720#define SSLv23_method TLS_method#define SSLv23_server_method TLS_server_method#defi..._tls_server_method
文章浏览阅读666次。EduSoho开源版 播放几分钟就 您的浏览器不能播放此视频,快进也会出现相同的问题。本地视频出现当前浏览器不能播放的原因:1、服务器 php 是否安装 fileinfo组件。2、视频格式编码不是MP4,H264编码,如果不清楚格式编码,用格式工厂重新转码输出MP4,H264编码;3、视频太大或者网络太慢,本地视频没有切片播放功能,加载完全时才能播放,如果在固定时间内没有加载完全,就会出现不能播放提示。处理建议:可以对接第三方云视频,例如阿里云点播、腾讯云点播、七牛云点播、保利威视点播等等._edusoho 视频加载遇到问题
文章浏览阅读74次。在第一课中我们己经介绍过,ActionScript是一部语言,即然是语言它就有它自己的语法[color="#993300"][1b]一、ActionScript语句是区分大小写的:[/1b][/color]在ActionScript中英语字母的大小写具有不同的意义的.我们来看一个例子:打开时间轴第一帧的动作面板,输入: Name="Sanbos"; name="假博士"; ..._前端as语法
文章浏览阅读468次。安装sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppasudo apt-get updatesudo apt-get install mosquittosudo apt-get updatesudo apt-get install mosquitto-devsudo apt-get install mosquitto-clients查看状态sudo service mosquitto status 本地测试#.._mtuptty搭建
文章浏览阅读4k次。TFT-LCD一、简介: TFT-LCD即薄膜晶体管液晶显示器,依据其尺寸、分辨率和驱动芯片的不同有很多分类,下边会依据2.8寸320X240分辨率以ILI9341芯片驱动的TFT-LCD做相关介绍。二、接口: 模块采用16位并方式与外部连接,其相关接口图及信号线功能如下: CS:TFTLCD片选信号。WR:向TFTLCD写数据。R_stm32f4 lcd控制器
文章浏览阅读117次。在分析数控机床进给伺服系统数学模型的基础上,采用一种基于人工鱼群算法优化PID控制器参数,并与传统的Ziegler-Nichols法进行比较.仿真实验结果表明:人工鱼群PID控制方法的效果明显优于传统的PID控制,具有良好的动态和稳态性能._鱼群算法 pid 代码