当前位置:   article > 正文

Java C++ 数据结构对比_cpp和java的数据结构

cpp和java的数据结构

转载请注明出处:https://blog.csdn.net/mymottoissh/article/details/82826709

目的是总结一下 JAVA 和 C++几种常用的数据结构内存模型,知道了内存结构,才能在实战中更好滴选用数据结构。通过总结,日后也避免把两种语言的结构混为一谈。闲言少叙,开始正文。

关键字:JAVA C++ 数据结构

目录

一、动态数组

二、链表

三、树


首先做一个整体的概括:

底层数据存储只分成两大类,一种是连续存储,如数组。一种是链式存储,如链表。在这基础之上,结合结构和算法,才衍生出如树、图之类的结构。前者的优点:随机寻址快。后者优点:插删速度快。这里也会结合源码做一些剖析。

一、动态数组

代表结构:

JAVA :ArrayList、Vector、HashMap/HashTable/LinkedHashMap、HashSet

C++ : vector、deque<特>、stack/queue <特>

开始接触的时候很困惑,Java里明明写的是list,为什么还算作数组。这就是恶心的地方,所以千万不要别外表欺骗了。

Java::ArrayList

ArrayList无疑是项目中最常用的结构,为了查找他底层的存储原理,直接看他的add方法

  1. /**
  2. * Appends the specified element to the end of this list.
  3. *
  4. * @param e element to be appended to this list
  5. * @return <tt>true</tt> (as specified by {@link Collection#add})
  6. */
  7. public boolean add(E e) {
  8. ensureCapacityInternal(size + 1); // Increments modCount!!
  9. elementData[size++] = e;
  10. return true;
  11. }

显然,存储结构为一数组 elementData。这里有一个关键方法实现动态内存

  1. private void ensureCapacityInternal(int minCapacity) {
  2. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  3. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  4. }
  5. ensureExplicitCapacity(minCapacity);
  6. }
  7. private void ensureExplicitCapacity(int minCapacity) {
  8. modCount++;
  9. // overflow-conscious code
  10. if (minCapacity - elementData.length > 0)
  11. grow(minCapacity);
  12. }
  13. private void grow(int minCapacity) {
  14. // overflow-conscious code
  15. int oldCapacity = elementData.length;
  16. int newCapacity = oldCapacity + (oldCapacity >> 1);
  17. if (newCapacity - minCapacity < 0)
  18. newCapacity = minCapacity;
  19. if (newCapacity - MAX_ARRAY_SIZE > 0)
  20. newCapacity = hugeCapacity(minCapacity);
  21. // minCapacity is usually close to size, so this is a win:
  22. elementData = Arrays.copyOf(elementData, newCapacity);
  23. }

每次添加一个数据之前,都要保证数组中有比现有数据长度多1的容量,如果保证不了,则会创建一个新的容量为以前1.5倍的数组,并把元素进行复制。可想而知,如果需要反复扩容的话,ArrayList的效率是非常低的。同时,ArrayList也保留了数组本身的特点:如果在某一位置增删数据,需要把后面的数据全都复制一遍。这部分代码就不贴了。

Java::Vector

  1. /**
  2. * Appends the specified element to the end of this Vector.
  3. *
  4. * @param e element to be appended to this Vector
  5. * @return {@code true} (as specified by {@link Collection#add})
  6. * @since 1.2
  7. */
  8. public synchronized boolean add(E e) {
  9. modCount++;
  10. ensureCapacityHelper(elementCount + 1);
  11. elementData[elementCount++] = e;
  12. return true;
  13. }

在add方法下,可以看到与ArrayList类似的存储:数组。增加之前,首先确保容量,唯一不同的在于在前面加入了synchronized关键字。所以在多线程环境下,应该选用Vector。

JAVA::HashMap/HashTable/LinkedHashMap

这里把HashMap及相关结归结到了动态数组里,这关系到HashMap的实现原理。直接放源码

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. /**
  5. * Implements Map.put and related methods
  6. *
  7. * @param hash hash for key
  8. * @param key the key
  9. * @param value the value to put
  10. * @param onlyIfAbsent if true, don't change existing value
  11. * @param evict if false, the table is in creation mode.
  12. * @return previous value, or null if none
  13. */
  14. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  15. boolean evict) {
  16. Node<K,V>[] tab; Node<K,V> p; int n, i;
  17. if ((tab = table) == null || (n = tab.length) == 0)
  18. n = (tab = resize()).length;
  19. if ((p = tab[i = (n - 1) & hash]) == null) //索引下标计算
  20. tab[i] = newNode(hash, key, value, null);
  21. else {
  22. Node<K,V> e; K k;
  23. if (p.hash == hash &&
  24. ((k = p.key) == key || (key != null && key.equals(k))))
  25. e = p;
  26. else if (p instanceof TreeNode)
  27. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  28. else {
  29. for (int binCount = 0; ; ++binCount) {
  30. if ((e = p.next) == null) {
  31. p.next = newNode(hash, key, value, null);
  32. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  33. treeifyBin(tab, hash);
  34. break;
  35. }
  36. if (e.hash == hash &&
  37. ((k = e.key) == key || (key != null && key.equals(k))))
  38. break;
  39. p = e;
  40. }
  41. }
  42. if (e != null) { // existing mapping for key
  43. V oldValue = e.value;
  44. if (!onlyIfAbsent || oldValue == null)
  45. e.value = value;
  46. afterNodeAccess(e);
  47. return oldValue;
  48. }
  49. }
  50. ++modCount;
  51. if (++size > threshold)
  52. resize();
  53. afterNodeInsertion(evict);
  54. return null;
  55. }

首先可以看到存储结构为一Node数组。在这里有点小疑问,Java里的Node是一个单向链表节点,而这里却用了一个Node数组存储。究其原因,还有分析一下HashMap的原理。当向map中put一个键值对时,首先通过hash方法计算key的哈希值,以 (数组长度-1)&哈希值 作为数组索引下标。如果该下标对应的数组值为空,则new一个新节点。如果已经有值,那该值对应的key一定等于新增的可以吗?当然不是,因为可能会有哈希碰撞。这时候就用到单向链表结构的特性了,如果当前的数组值对应的key与新增key不相等,则会遍历以当前Node值为首节点的链表,直到找到与新key相等的节点为止。其中TREEIFY_THRESHOLD 定义为8,如果超过次链表长度,将转为树存储。更形象的表示,HashMap的结构应该是这样滴。

在put源码中,有两个HashMap未实现的函数,但是在LinkedHashMap中实现了,这也保证了其有序的元素存储。

  1. // Callbacks to allow LinkedHashMap post-actions
  2. void afterNodeAccess(Node<K,V> p) { }
  3. void afterNodeInsertion(boolean evict) { }
  4. void afterNodeRemoval(Node<K,V> p) { }

至于HashTable与HashMap的区别,这里就不造轮子了

引用一段 : https://blog.csdn.net/qq_29631809/article/details/72599708

HashMap和Hashtable的主要的区别有:线程安全性,同步(synchronization),以及速度。

  • HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。 HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。
  • 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。 HashMap不能保证随着时间的推移Map中的元素次序是不变的。

Java::HashSet

废话不多说,贴一个源码就知道了

  1. /**
  2. * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
  3. * default initial capacity (16) and load factor (0.75).
  4. */
  5. public HashSet() {
  6. map = new HashMap<>();
  7. }
  8. /**
  9. * Adds the specified element to this set if it is not already present.
  10. * More formally, adds the specified element <tt>e</tt> to this set if
  11. * this set contains no element <tt>e2</tt> such that
  12. * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
  13. * If this set already contains the element, the call leaves the set
  14. * unchanged and returns <tt>false</tt>.
  15. *
  16. * @param e element to be added to this set
  17. * @return <tt>true</tt> if this set did not already contain the specified
  18. * element
  19. */
  20. public boolean add(E e) {
  21. return map.put(e, PRESENT)==null;
  22. }

HashSet内部维护了一个HashMap的,对HashSet的操作,实际上就是多HashMap的操作。

C++::vector

  1. void push_back(_Ty&& _Val)
  2. { // insert element at end
  3. if (_Inside(_STD addressof(_Val)))
  4. { // push back an element
  5. size_type _Idx = _STD addressof(_Val) - this->_Myfirst;
  6. if (this->_Mylast == this->_Myend)
  7. _Reserve(1);
  8. _Orphan_range(this->_Mylast, this->_Mylast);
  9. _Cons_val(this->_Alval,
  10. this->_Mylast,
  11. _STD forward<_Ty>(this->_Myfirst[_Idx]));
  12. ++this->_Mylast;
  13. }
  14. else
  15. { // push back a non-element
  16. if (this->_Mylast == this->_Myend)
  17. _Reserve(1);
  18. _Orphan_range(this->_Mylast, this->_Mylast);
  19. _Cons_val(this->_Alval,
  20. this->_Mylast,
  21. _STD forward<_Ty>(_Val));
  22. ++this->_Mylast;
  23. }
  24. }

pushback时,首先判断该元素地址是否属于数组内部,如果是,则直接在改地址对应位置更新数据,同时在最末端位置压入数据。如果不是,则在数组末端添加数据。

首先,vector是单端数组,只提供了push_back函数,虽然也提供了insert函数,但是插入时,会导致后续元素进行交换反转,所以插入效率极低。

其次,_Reserve函数用于数组扩容,扩大为原来的1.5倍。在扩容的过程中会new一块新的内存空间。这意味着一旦被扩容,指向原有元素的迭代器将失效。

C++::deque

双端数组,提供前端压入函数push_front和末端压入函数push_back。虽然命名为双端数组,而且从某种意义上来说是内存连续的,但是关于deque有必要重点说明一下。首先来做一个实验。

有如下代码,编译运行

  1. int main() {
  2. std::deque<int> deq;
  3. for(int i = 0; i < 10; i++) {
  4. deq.push_back(i);
  5. }
  6. for(int i = 0; i < 10; i++) {
  7. cout << deq[i] << " " << &deq[i] << endl;
  8. }
  9. system("pause");
  10. return 0;
  11. }

来看一运行结果

  1. 0 00000000002CAF10
  2. 1 00000000002CAF14
  3. 2 00000000002CAF18
  4. 3 00000000002CAF1C
  5. 4 00000000002CAF60
  6. 5 00000000002CAF64
  7. 6 00000000002CAF68
  8. 7 00000000002CAF6C
  9. 8 00000000002DD2D0
  10. 9 00000000002DD2D4

奇怪吧,从运行结果可以看到,每四个元素的内存是连续的,从第五个开始,会跳过一段空间,重新组织内存。那关于deque实现到底是怎样的呢。用一句话说,deque是数组和指针(并不能说是链表)的组合实现。上源码。

  1. void push_front(_Ty&& _Val)
  2. { // insert element at beginning
  3. this->_Orphan_all();
  4. _PUSH_FRONT_BEGIN;
  5. _Cons_val(this->_Alval,
  6. this->_Map[_Block] + _Newoff % _DEQUESIZ,
  7. _STD forward<_Ty>(_Val));
  8. _PUSH_FRONT_END;
  9. }
  10. void push_back(_Ty&& _Val)
  11. { // insert element at end
  12. this->_Orphan_all();
  13. _PUSH_BACK_BEGIN;
  14. _Cons_val(this->_Alval,
  15. this->_Map[_Block] + _Newoff % _DEQUESIZ,
  16. _STD forward<_Ty>(_Val));
  17. _PUSH_BACK_END;
  18. }

从源码中可以看到,插入的时候,实际上是取的_Map[_Block]所存储的地址。

_Mapptr _Map;		// pointer to array of pointers to blocks

 而通过注释看到,_Map实际上是一个二级指针,指向的是一个数组,这个数组存放的是指向block的指针。那么问题又来了,block是什么鬼。

  1. size_type _Getblock(size_type _Off) const
  2. { // determine block from offset
  3. // NB: _Mapsize and _DEQUESIZ are guaranteed to be powers of 2
  4. return ((_Off / _DEQUESIZ) & (_Mapsize - 1));
  5. }

 看看这个获取block的函数就大致明白了,block是一块存储内存。其实在deque内部,数组里的每个元素存储的都是一个指向这种block的指针。block是一段连续的存储空间,在block内部的元素,内存都是连续的,但是block与block之间内存不连续。那么block的大小是怎么确定的呢。

  1. #define _DEQUESIZ (sizeof (value_type) <= 1 ? 16 \
  2. : sizeof (value_type) <= 2 ? 8 \
  3. : sizeof (value_type) <= 4 ? 4 \
  4. : sizeof (value_type) <= 8 ? 2 \
  5. : 1) /* elements per block (a power of 2) */

 就是通过上述这个算法计算得到。这也解释了为什么我们实验中,针对deque<int>对象,block的大小为4。

另外,关于前插和后插,offset的计算可以区分。在插入数据的前后,分别通过宏定义做了一些操作,相信这就是区分前还是后的关键。来看一下宏定义中都做了什么。

  1. #define _PUSH_FRONT_BEGIN \
  2. if (this->_Myoff % _DEQUESIZ == 0 \
  3. && this->_Mapsize <= (this->_Mysize + _DEQUESIZ) / _DEQUESIZ) \
  4. _Growmap(1); \
  5. size_type _Newoff = this->_Myoff != 0 ? this->_Myoff \
  6. : this->_Mapsize * _DEQUESIZ; \
  7. size_type _Block = --_Newoff / _DEQUESIZ; \
  8. if (this->_Map[_Block] == 0) \
  9. this->_Map[_Block] = this->_Alval.allocate(_DEQUESIZ)
  1. #define _PUSH_BACK_BEGIN \
  2. if ((this->_Myoff + this->_Mysize) % _DEQUESIZ == 0 \
  3. && this->_Mapsize <= (this->_Mysize + _DEQUESIZ) / _DEQUESIZ) \
  4. _Growmap(1); \
  5. size_type _Newoff = this->_Myoff + this->_Mysize; \
  6. size_type _Block = _Newoff / _DEQUESIZ; \
  7. if (this->_Mapsize <= _Block) \
  8. _Block -= this->_Mapsize; \
  9. if (this->_Map[_Block] == 0) \
  10. this->_Map[_Block] = this->_Alval.allocate(_DEQUESIZ)

对比发现,逻辑基本一致。首先判断容量是否够用,不够用则扩容。然后计算偏移量,即写入数据的位置,最后调用_Cons_val写入数据。不过这个代码是有讲究的,在分析源码之前,我对这个结构也有些误解。

首先是扩容判断条件。既然是双端数组,以前认为首末两端都会预留一些空位用于写入数据,所以如果有一端空间不足,则会扩容。而实际的情况是,容量不分前后,只有在总容量不够用时才会重新分配。

那么第二个问题来了,既然不是首末都有空位,那怎么实现push_back和push_front呢。这里deque维护了几个成员变量

  1. _Mapptr _Map; // 指向block指针数组的指针
  2. size_type _Mapsize; // _Map的尺寸
  3. size_type _Myoff; // 首元素的偏移地址
  4. size_type _Mysize; // 当前数组的尺寸

其中,_Myoff记录了首元素的偏移位置,而且从宏_PUSH_FRONT_BEGIN的源码中可以看到,一旦首部空间不足,则会将push_front的数据写加到数据最末端,从这角度来讲,deque类似于一个循环数组。所以这是一个圈圈,而不是一条直线。

同时,与vector一样,一旦内存因扩容而重新分配,指向原有元素的迭代器将失效。

C++::stack/queue

把这两个放在一起说,一个先进后出,一个先进先出。两个结构内部使用的容器其实都是deque,所以不再深入分析了。.

class _Container = deque<_Ty> >

二、链表

代表结构

JAVA :LinkedList

C++ : list

Java::LinkedList

  1. /**
  2. * Appends the specified element to the end of this list.
  3. *
  4. * <p>This method is equivalent to {@link #addLast}.
  5. *
  6. * @param e element to be appended to this list
  7. * @return {@code true} (as specified by {@link Collection#add})
  8. */
  9. public boolean add(E e) {
  10. linkLast(e);
  11. return true;
  12. }
  13. /**
  14. * Links e as last element.
  15. */
  16. void linkLast(E e) {
  17. final Node<E> l = last;
  18. final Node<E> newNode = new Node<>(l, e, null);
  19. last = newNode;
  20. if (l == null)
  21. first = newNode;
  22. else
  23. l.next = newNode;
  24. size++;
  25. modCount++;
  26. }

单向链表。所以在进行LinkedList随机存取时,需要遍历链表。

C++::list

双向链表,结构不多说。这里需要重点说明一下,C++的list到底是不是循环链表呢?是,但不是典型的循环链表,因为在首Node和尾Node之间存在一个"多余"的节点。

_Nodeptr _Myhead;	// pointer to head node

就是它。也是因为有它,当我们用迭代器遍历元素时,才不会遍历个没完没了。原因看这

  1. iterator end()
  2. { // return iterator for end of mutable sequence
  3. return (iterator(this->_Myhead, this));
  4. }

end返回的正是他的地址。如果画出来的话,C++中list的数据结构应该是这样子滴。画的糙,凑合看。

三、树

代表结构

JAVA :TreeMap

C++ : set/map

Java::TreeMap

  1. /**
  2. * The comparator used to maintain order in this tree map, or
  3. * null if it uses the natural ordering of its keys.
  4. *
  5. * @serial
  6. */
  7. private final Comparator<? super K> comparator;
  8. private transient Entry<K,V> root;
  9. /**
  10. * Associates the specified value with the specified key in this map.
  11. * If the map previously contained a mapping for the key, the old
  12. * value is replaced.
  13. *
  14. * @param key key with which the specified value is to be associated
  15. * @param value value to be associated with the specified key
  16. *
  17. * @return the previous value associated with {@code key}, or
  18. * {@code null} if there was no mapping for {@code key}.
  19. * (A {@code null} return can also indicate that the map
  20. * previously associated {@code null} with {@code key}.)
  21. * @throws ClassCastException if the specified key cannot be compared
  22. * with the keys currently in the map
  23. * @throws NullPointerException if the specified key is null
  24. * and this map uses natural ordering, or its comparator
  25. * does not permit null keys
  26. */
  27. public V put(K key, V value) {
  28. Entry<K,V> t = root;
  29. if (t == null) {
  30. compare(key, key); // type (and possibly null) check
  31. root = new Entry<>(key, value, null);
  32. size = 1;
  33. modCount++;
  34. return null;
  35. }
  36. int cmp;
  37. Entry<K,V> parent;
  38. // split comparator and comparable paths
  39. Comparator<? super K> cpr = comparator;
  40. if (cpr != null) {
  41. do {
  42. parent = t;
  43. cmp = cpr.compare(key, t.key);
  44. if (cmp < 0)
  45. t = t.left;
  46. else if (cmp > 0)
  47. t = t.right;
  48. else
  49. return t.setValue(value);
  50. } while (t != null);
  51. }
  52. else {
  53. if (key == null)
  54. throw new NullPointerException();
  55. @SuppressWarnings("unchecked")
  56. Comparable<? super K> k = (Comparable<? super K>) key;
  57. do {
  58. parent = t;
  59. cmp = k.compareTo(t.key);
  60. if (cmp < 0)
  61. t = t.left;
  62. else if (cmp > 0)
  63. t = t.right;
  64. else
  65. return t.setValue(value);
  66. } while (t != null);
  67. }
  68. Entry<K,V> e = new Entry<>(key, value, parent);
  69. if (cmp < 0)
  70. parent.left = e;
  71. else
  72. parent.right = e;
  73. fixAfterInsertion(e);
  74. size++;
  75. modCount++;
  76. return null;
  77. }

首先TreeMap是一棵红黑树,类的内部维护了两个主要变量:root节点和比较器(谓词)。root为红黑树的树根,当插入一个数据时,根据比较器判断向左子树还是右子树插入数据。构造TreeMap时,可以传入比较器,如果比较器为空,则使用类型默认的比较器。所如果key为自定义类型,则最好实现Comparable接口。

C++::set/map

C++中的set和map都继承自xtree,也就是说两种结构都是用红黑树存储的。我们知道,这两种存储都是有序的,且存在默认的排序规则,在构造时,可以自定义仿函数来重定义排序规则。大体上看一下xtree插入的逻辑。

  1. _Pairib insert(const value_type& _Val, bool _Leftish)
  2. { // try to insert node with value _Val, on left if _Leftish
  3. _Nodeptr _Trynode = _Root();
  4. _Nodeptr _Wherenode = this->_Myhead;
  5. bool _Addleft = true; // add to left of head if tree empty
  6. while (!this->_Isnil(_Trynode))
  7. { // look for leaf to insert before (_Addleft) or after
  8. _Wherenode = _Trynode;
  9. if (_Leftish)
  10. _Addleft = !_DEBUG_LT_PRED(this->comp,
  11. this->_Key(_Trynode),
  12. this->_Kfn(_Val)); // favor left end
  13. else
  14. _Addleft = _DEBUG_LT_PRED(this->comp,
  15. this->_Kfn(_Val),
  16. this->_Key(_Trynode)); // favor right end
  17. _Trynode = _Addleft ? this->_Left(_Trynode)
  18. : this->_Right(_Trynode);
  19. }
  20. if (this->_Multi)
  21. return (_Pairib(_Insert(_Addleft, _Wherenode, _Val), true));
  22. else
  23. { // insert only if unique
  24. iterator _Where = iterator(_Wherenode, this);
  25. if (!_Addleft)
  26. ; // need to test if insert after is okay
  27. else if (_Where == begin())
  28. return (_Pairib(_Insert(true, _Wherenode, _Val), true));
  29. else
  30. --_Where; // need to test if insert before is okay
  31. if (_DEBUG_LT_PRED(this->comp,
  32. this->_Key(_Where._Mynode()),
  33. this->_Kfn(_Val)))
  34. return (_Pairib(_Insert(_Addleft, _Wherenode, _Val), true));
  35. else
  36. return (_Pairib(_Where, false));
  37. }
  38. }

插入时,首先通过仿函数判断插入左子树还是右子树。然后判断该位置是否有数据,没有的话才会执行插入。最后调用_Pairlib进行数据的添加。

以上,对Java和C++常用的数据结构做了简要的总结。日后选用结构时可以根据其特性,选却最优的建模方式。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/227402
推荐阅读
相关标签
  

闽ICP备14008679号