自定义数组及简单时间复杂度分析

前言:

作为java的一种容器,数组的优缺点同样明显

优点:使用简单 ,查询效率高,内存为连续的区域 

缺点:大小固定,不适合动态存储,不方便动态添加

一、自定义实现数组

1、Java中定义数组的三种形式

 
  1. // 第一种:数组格式 类型[] 数组名 = new 类型[数组长度]

  2. int[] arr = new int[10];

  3.  
  4. // 第二种:定义数组,直接赋值

  5. int[] arr = new int[]{11, 22, 33, 44};

  6.  
  7. // 第三种:定义数组,直接赋值

  8. int[] arr = {11, 22, 33, 44};

提示:第三种定义方式最常用

2、自定义数组

为了加深对数组的理解,实现自定义数组,定义数组的动态扩减容,且自动维护数组的大小size,从而节省了内存空间资源。

自定义数组及简单时间复杂度分析

 自定义数组的完整代码

 
  1. package com.theone.arrays;

  2.  
  3. /**

  4. * @Description TODO 自定义数组

  5. * @Author Pureman

  6. * @Date 2018/9/1 13:08

  7. **/

  8. public class MyArray<E> {

  9.  
  10. // 定义数组类型

  11. private E[] data;

  12. // 特别提示一点,这里的数组容量默认是10,因此使用data.length不是数组真实的长度

  13. // 定义数组大小,实现动态管理数组长度

  14. private int size;

  15.  
  16. // 构造函数,传入数组的容量capacity构造Array

  17. public MyArray(int capacity) {

  18. this.data = (E[]) new Object[capacity];

  19. this.size = 0;

  20. }

  21.  
  22. // 空参构造,默认数组容量为10

  23. public MyArray() {

  24. this(10);

  25. }

  26.  
  27. // 获取数组大小

  28. public int getSize() {

  29. return this.size;

  30. }

  31.  
  32. // 获取数组的容量

  33. public int getCapacity() {

  34. return this.data.length;

  35. }

  36.  
  37. // 判断数组是否为空

  38. public Boolean isEmpty() {

  39. return this.size == 0;

  40. }

  41.  
  42. // 定义方法,向数组的最后添加元素

  43. public void addLast(E e) {

  44. add(size, e);

  45. }

  46.  
  47. // 定义方法,向数组的0索引位置添加元素

  48. public void addFirst(E e) {

  49. add(0, e);

  50. }

  51.  
  52. // 定义方法向数组中的任意位置添加元素

  53. public void add(int index, E e) {

  54. if (index < 0 || index > size) {

  55. throw new IllegalArgumentException("Add failed,index is not expected");

  56. }

  57. if (size == data.length) {

  58. this.resize(2 * data.length);

  59. }

  60. // 遍历数据,将原来数据从index向后移动一位

  61. for (int i = size - 1; i >= index; i--) {

  62. data[i + 1] = data[i];

  63. }

  64. // 覆盖原下标的元素

  65. data[index] = e;

  66. // 数组大小加一

  67. size++;

  68. }

  69.  
  70. // 获取数组中指定下标的元素

  71. public E get(int index) {

  72. if (index == size) {

  73. throw new IllegalArgumentException("Get failed ,because required index is not legal");

  74. }

  75. return data[index];

  76. }

  77.  
  78. // 更新数组中指定角标的元素

  79. public void setIndex(int index, E e) {

  80. if (index == size) {

  81. throw new IllegalArgumentException("update failed ,because required index is not legal");

  82. }

  83. data[index] = e;

  84. }

  85.  
  86. // 判断数组中是否包含指定元素

  87. public Boolean contains(E e) {

  88. for (int i = 0; i < size; i++) {

  89. // ==比较的是对象的引用,equeals比较内容

  90. if (data[i].equals(e)) {

  91. return true;

  92. }

  93. }

  94. return false;

  95. }

  96.  
  97. // 获取元素的索引

  98. public int search(E e) {

  99. for (int i = 0; i < size; i++) {

  100. if (data[i].equals(e)) {

  101. return i;

  102. }

  103. }

  104. return -1;

  105. }

  106.  
  107. // 指定索引,删除数组中的元素,并返回被删除的元素值

  108. public E remove(int index) {

  109. if (0 > index || size <= index) {

  110. throw new IllegalArgumentException("Remove failed.Index is illegal");

  111. }

  112. // 获取待被待删除元素

  113. E ret = data[index];

  114. // 不需要关注data[size-1]的元素是否还有值,数组封装对外部不暴露,因此调用时并不知道该数组下标为size-1的位置还有元素

  115. for (int i = index + 1; i < size; i++) {

  116. data[i - 1] = data[i];

  117. }

  118. size--;

  119. // loitering object(游荡的对象) != memory leak(内存泄漏)

  120. data[size] = null;

  121. // 判断数组长度是否是容量的1/4,如果是,实现动态减容

  122. if (size == data.length / 4 && data.length / 2 != 0) {

  123. this.resize(data.length / 2);

  124. }

  125. // 返回删除的元素

  126. return ret;

  127. }

  128.  
  129. // 移除数组中的第一个元素

  130. public E removeFirst() {

  131. return this.remove(0);

  132. }

  133.  
  134. //移除数组中的最后一个元素

  135. public E removeLast() {

  136. return this.remove(this.getSize() - 1);

  137. }

  138.  
  139. // 移除数组中指定元素

  140. public void removeElement(E e) {

  141. int index = this.search(e);

  142. if (1 != index) {

  143. this.remove(index);

  144. }

  145. }

  146.  
  147. @Override

  148. public String toString() {

  149. StringBuffer sb = new StringBuffer();

  150. sb.append(String.format("Array:size=%d,capacity=%d\n", size, data.length));

  151. sb.append("[");

  152. for (int i = 0; i < size; i++) {

  153. sb.append(data[i]);

  154. if (i != size - 1) {

  155. sb.append(" ,");

  156. }

  157. }

  158. sb.append("]");

  159. return sb.toString();

  160. }

  161.  
  162. // 动态拓展数组容量

  163. private void resize(int capacity) {

  164. E[] newArr = (E[]) new Object[capacity];

  165. for (int i = 0; i < size; i++) {

  166. newArr[i] = data[i];

  167. }

  168. data = newArr;

  169. }

  170. }

二、简单介绍时间复杂度

简单时间复杂度分为
O(1)、O(n)、O(lgn)、O(nlogn)、O(n^2)
注意:大O描述的是算法的运行时间和输入数据之间的关系

 
  1. public static int sum(int[] nums){

  2. int sum = 0 ;

  3. for(int num : nums){

  4. sum += num ;

  5. }

  6. return sum ;

  7. }

sum(int[] nums)方法的时间复杂度为O(n),实际复杂度公式为:T = c1*n + c2

参数解释:

  • n: nums中元素的个数
  • c2:指sum初始化的时间,及返回值sum返回的时间
  • c1:for循环内遍历num,与sum累加的时间

为什么要使用大O,叫做O(n)?    

在时间复杂度分析过程中,都会假设n趋势于无穷,此时c2所消耗的时间远远小于c1*n的时间,因此分析时忽略常数,此时算法和n呈线性关系

提示:计算时间复杂度都是趋向于最坏情况

另外两个简单的时间复杂度概念:均摊复杂度分析、防止复杂度震荡

均摊复杂度分析(amortized time complexity):resize()

当size == data.length时,自动调用方法resize(2 * data.length)实现数组的动态扩容,扩容后数组的容量是原来的二倍,因此不是每次添加元素都会调用resize()方法进行扩容,所以当自定义数组中的方法addLast()执行所消耗的时间,因由数组arr中的每个元素均应分摊。例如:数组arr的大小为9时,其每个元素所执行的基本操作为15/9次操作

 
  1. public void test02() {

  2. // 定义数组容量为6

  3. MyArray<Integer> arr = new MyArray<Integer>(6);

  4. // 向数组添加8个元素

  5. for (int i = 0; i < 8; i++) {

  6. arr.addLast(i);

  7. }

  8. System.out.println("arr = " + arr);

  9. // 向数组的最后碎银添加元素100,此时数组大小为9

  10. arr.addFirst(100);

  11. System.out.println("arr = " + arr);

  12. }

防止复杂度震荡:在resize()扩容后,立刻删除数组中的元素,触发数组自动减容

下面代码演示了复杂度震荡,数组动态扩容后,立即删除元素,数组动态减容,这样会造成时间复杂度增加

 
  1. public void test03() {

  2. // 定义数组容量为6

  3. MyArray<Integer> arr = new MyArray<Integer>(5);

  4. for (int i = 0; i < 5; i++) {

  5. arr.addLast(i);

  6. }

  7. // 添加元素,此时数组需要动态扩容

  8. arr.addLast(100);

  9. // 动态扩容后,立即删除元素,数组需要动态减容

  10. arr.removeLast();

  11. }

为了防止复杂度震荡,避免删除元素后,造成复杂度震荡,采用延迟动态减容,代码如下

 
  1. public E remove(int index) {

  2. if (0 > index || size <= index) {

  3. throw new IllegalArgumentException("Remove failed.Index is illegal");

  4. }

  5. // 不需要关注data[size-1]的元素是否还有值,数组封装对外部不暴露,因此调用时并不知道该数组下标为size-1的位置还有元素

  6. for (int i = index + 1; i < size; i++) {

  7. data[i - 1] = data[i];

  8. }

  9. size--;

  10. // loitering object(游荡的对象) != memory leak(内存泄漏)

  11. data[size] = null;

  12. // 判断数组长度是否是容量的1/4,如果是,实现动态减容

  13. if (size == data.length / 4 && data.length / 2 != 0) {

  14. this.resize(data.length / 2);

  15. }

  16. return data[index];

  17. }