我理解的数据结构(一)—— 数组(Array)

首先,我是一个phper,但是毕竟php是一个脚本语言,如果使用脚本语言去理解数据结构具有一定的局限性。因为脚本语言是不需要编译的,如果你的语法写的不错,可能执行起来会要比用一个更好的数据结构来的更快、更高效(在数据量不大的情况下)。而且数据结构是脱离任何一门语言存在的。所以,下面会选用java去更深入的理解数据结构。

注:这里不会去过多的解释java的语法。

一、定义一个数组的两种方式

  • int[] arr = new int[10];
  • int[] arr = new int[] {10, 20, 30};

二、数组基础

  • 数组的容量在数组一开始定义的时候就固定了。
  • 数组最大的优点:根据索引快速查询。如:arr[2]
  • 数组最好应用于“索引有语意”的情况下。
  • 但并非所有有语意的索引都适用于数组:比如索引是一个人的身份证号,会开辟过大的空间,不现实。
  • 下面会讨论数组“索引没有语意”的情况,基于java数组,二次封装属于我们自己的数组类,更深入的理解数组。

三、创建一个最基本的数组类

学习任何一个数据结构,CRUD必不可少。下面,让我们来一起一步步完善属于我们自己的数组的增、删、改、查

```
public class Array {

// 数组的实际大小
private int size;
// 数组
private int[] data; // 构造函数,根据传入的容纳量定义一个int类型的数组
public Array(int capacity) {
data = new int[capacity];
size = 0;
} // 重载,没有传入容纳量,定义一个长度为10的int类型数组
public Array() {
this(10);
} // 数组的实际大小
public int getSize() {
return size;
} // 数组的容纳量
public int getCapacity() {
return data.length;
} // 数组是否为空
public boolean isEmpty() {
return size == 0;
}

}


<h3>四、增</h3>

//往数组的任意位置插入

public void add(int index, int ele) {

// 数组已满
if (size == data.length) {
throw new IllegalArgumentException("add failed. arr is full");
} // 插入的索引位不合法
if (index &lt; 0 || index &gt;= size) {
throw new IllegalArgumentException("add failed. index &lt; 0 or index &gt;= size");
} // 从index向后的所有元素均向后赋值
for (int i = size - 1; i &gt;= index; i--) {
data[i + 1] = data[i];
}
data[index] = ele;
size++;

}

// 第一个位置插入

public void addFirst(int ele) {

add(0, ele);

}

// 最后一个位置插入

public void addLast(int ele) {

add(size, ele);

}


<h3>五、查和改</h3>

// 查询index索引位置的元素

public int get(int index) {

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

throw new IllegalArgumentException("get failed. index is illegal");

}

return data[index];

}

// 查询ele元素的索引,不存在返回-1

public int find(int ele) {

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

if (data[i] == ele) {

return i;

}

}

return -1;

}

// 更新Index的元素

public void set(int index, int ele) {

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

throw new IllegalArgumentException("get failed. index is illegal");

}

data[index] = ele;

}


<h3>六、删</h3>

// 根据索引删除数组中的第一个ele,返回ele

public int remove(int index) {

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

throw new IllegalArgumentException("remove failed. index is illegal");

}

for (int i = index + 1; i &lt; size; i++) {
data[i - 1] = data[i];
}
size--; return data[index];

}

// 删除第一个元素

public int removeFirst() {

return remove(0);

}

// 删除最后一个

public int removeLast() {

return remove(size - 1);

}

// 删除指定元素

public void removeElement(int ele) {

int index = find(ele);

if (index != -1) {

remove(index);

}

}


<h3>七、包含和重写toString</h3>

Override

public String toString() {

StringBuffer res = new StringBuffer();

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

res.append("[");

for (int i = 0; i &lt; size; i++) {

    res.append(data[i]);
if (i != size - 1) {
res.append(", ");
}
}
res.append("]");
return res.toString();

}

// 查询数组中是否包含元素ele

public boolean contain(int ele) {

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

if (data[i] == ele) {

return true;

}

}

return false;

}


<p><strong>注:</strong>通过以上方法我们已经创建了一个<strong>最最最最最</strong>基本的数组类(见下图)。当然,你也可以去添加一些自己需要的方法,例如:<code>removeAll</code>、<code>findAll</code>之类的。<br></p> ![](https://img2018.cnblogs.com/blog/1504257/201811/1504257-20181117174850402-826338619.png) <blockquote>但是,我们现在的数组只支持int类型,太过局限。接下来,我们去给我们的数组升华一哈~</blockquote>
<h3>八、使用泛型让我们的数组支持“任意”数据类型</h3>
<blockquote>首先,为什么我要在<strong>任意</strong>这两个字加上引号,因为java的泛型不支持基本数据类型,只能是类的对象。<br>但是,这并不代表如果我们使用了泛型,就不可以使用基本数据类型了,因为每一个基本数据类型都有一个对应的<strong>包装类</strong>。<br>使用泛型的时候,我们只需要传入对应的包装类即可。</blockquote>
<h4>java的基本数据类型</h4>
<table>
<thead><tr>
<th align="center">基本数据类型</th>
<th align="center">包装类</th>
</tr></thead>
<tbody>
<tr>
<td align="center">boolean</td>
<td align="center">Boolean</td>
</tr>
<tr>
<td align="center">byte</td>
<td align="center">Byte</td>
</tr>
<tr>
<td align="center">char</td>
<td align="center">Char</td>
</tr>
<tr>
<td align="center">short</td>
<td align="center">Short</td>
</tr>
<tr>
<td align="center">int</td>
<td align="center">Int</td>
</tr>
<tr>
<td align="center">long</td>
<td align="center">Long</td>
</tr>
<tr>
<td align="center">float</td>
<td align="center">Float</td>
</tr>
<tr>
<td align="center">double</td>
<td align="center">Double</td>
</tr>
</tbody>
</table>
<h4>所以,我们的代码只需要进行极小的改动即可:</h4>
public class ArrayNew&lt;E&gt; {
// 数组的实际大小
private int size;
// 数组
private E[] data; // 构造函数,根据传入的容纳量定义一个 E 类型的数组
public ArrayNew(int capacity) {
// 强转
data = (E[]) new Object[capacity];
size = 0;
} // 重载,没有传入容纳量,定义一个长度为10的int类型数组
public ArrayNew() {
this(10);
} // 数组的实际大小
public int getSize() {
return size;
} // 数组的容纳量
public int getCapacity() {
return data.length;
} // 数组是否为空
public boolean isEmpty() {
return size == 0;
} // 往数组的任意位置插入
public void add(int index, E ele) { // 数组已满
if (size == data.length) {
throw new IllegalArgumentException("add failed. arr is full");
} // 插入的索引位不合法
if (index &lt; 0 || index &gt; size) {
throw new IllegalArgumentException("add failed. index &lt; 0 or index &gt; size");
} // 从index向后的所有元素均向后赋值
for (int i = size - 1; i &gt;= index; i--) {
data[i + 1] = data[i];
}
data[index] = ele;
size++;
} // 第一个位置插入
public void addFirst(E ele) {
add(0, ele);
} // 最后一个位置插入
public void addLast(E ele) {
add(size, ele);
} // 查询index索引位置的元素
public E get(int index) {
if (index &lt; 0 || index &gt;= size) {
throw new IllegalArgumentException("get failed. index is illegal");
}
return data[index];
} // 查询ele元素的索引,不存在返回-1
public int find(E ele) {
for (int i = 0; i &lt; size; i++) {
if (data[i].equals(ele)) {
return i;
}
}
return -1;
} // 更新Index的元素
public void set(int index, E ele) {
if (index &lt; 0 || index &gt;= size) {
throw new IllegalArgumentException("get failed. index is illegal");
}
data[index] = ele;
} // 根据索引删除数组中的第一个ele,返回ele
public E remove(int index) {
if (index &lt; 0 || index &gt;= size) {
throw new IllegalArgumentException("remove failed. index is illegal");
} E result = data[index];
for (int i = index + 1; i &lt; size; i++) {
data[i - 1] = (data[i]);
}
// 空间释放,垃圾回收会自动回收
data[--size] = null; return result;
} // 删除第一个元素
public E removeFirst() {
return remove(0);
} // 删除最后一个
public E removeLast() {
return remove(size - 1);
} // 删除指定元素
public void removeElement(E ele) {
int index = find(ele);
if (index != -1) {
remove(index);
}
} // 查询数组中是否包含元素ele
public boolean contain(E ele) {
for (int i = 0; i &lt; size; i++) {
if (data[i].equals(ele)) {
return true;
}
}
return false;
} @Override
public String toString() {
StringBuffer res = new StringBuffer();
res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
res.append("["); for (int i = 0; i &lt; size; i++) { res.append(data[i]);
if (i != size - 1) {
res.append(", ");
}
}
res.append("]");
return res.toString();
}

}


<p><strong>注:</strong>创建数组时,只需<code>ArrayNew&lt;Student&gt; arr = new ArrayNew&lt;&gt;(20);</code>即可。</p>
<h3>九、动态数组</h3>
<blockquote>
<strong>原理:</strong>其实,动态数组的原理非常简单,如果我们希望我们的数组具有可伸缩性,只需要我们在添加或者删除元素时判断<code>size</code>是否到达临界。然后去创建一个新<code>capacity</code>的数组,然后把旧数组的引用指向新数组即可。<br>所以,我们上述代码的改变极小,只需要改变<code>add</code>、<code>remove</code>即可。然后添加一个<code>resize</code>方法。</blockquote>

// 往数组的任意位置插入

public void add(int index, E ele) {

// 插入的索引位不合法

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

throw new IllegalArgumentException("add failed. index < 0 or index > size");

}

// 如果size == data.length,数组长度已满
if (size == data.length) {
resize(data.length * 2);
} // 从index向后的所有元素均向后赋值
for (int i = size - 1; i &gt;= index; i--) {
data[i + 1] = data[i];
}
data[index] = ele;
size++;

}

// 根据索引删除数组中的第一个ele,返回ele

public E remove(int index) {

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

throw new IllegalArgumentException("remove failed. index is illegal");

}

E result = data[index];
for (int i = index + 1; i &lt; size; i++) {
data[i - 1] = (data[i]);
}
// 空间释放,垃圾回收会自动回收
data[--size] = null; // 减小数组长度,不要浪费空间
if (size == data.length / 2 &amp;&amp; size != 0) {
resize(size);
} return result;

}

// 自动伸缩数组

private void resize(int newCapacity) {

E[] newData = (E[])new Object[newCapacity];

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

newData[i] = data[i];

}

data = newData;

}


<h3>十、简单复杂度分析我们封装的数组</h3>
<blockquote>通过上面的分析和代码实现,我们封装了一个自己的数组,并且实现了一些数组<strong>最基本</strong>的功能,包括支持增、删、改、查、支持任意数据类型以及动态数组。那么我们就来分析一下我们自己封装数组的复杂度。</blockquote>
<table>
<thead><tr>
<th align="center">操作</th>
<th align="center">复杂度</th>
</tr></thead>
<tbody>
<tr>
<td align="center">增</td>
<td align="center">O(n)</td>
</tr>
<tr>
<td align="center">删</td>
<td align="center">O(n)</td>
</tr>
<tr>
<td align="center">改</td>
<td align="center">已知索引O(1);未知索引O(n)</td>
</tr>
<tr>
<td align="center">查</td>
<td align="center">已知索引O(1);未知索引O(n)</td>
</tr>
</tbody>
</table>
<p><strong>但是:</strong>在我们的数组中,增和删我们都调用了<code>resize</code>方法,如果<code>size &lt; data.length</code>,其实我们执行<code>addLast</code>复杂度只是<code>O(1)</code>而已(<code>removeLast</code>同理)。所以,我们应该怎么去分析<code>resize</code>方法所带来的复杂度呢?</p>
<h3>十一、均摊复杂度和防止复杂度的震荡</h3>
<h4>(1)均摊复杂度</h4>
<blockquote>让我们拿 <strong>增</strong> 来举例</blockquote>
<table>
<thead><tr>
<th align="center">方法</th>
<th align="center">复杂度</th>
</tr></thead>
<tbody>
<tr>
<td align="center">addLast(ele)</td>
<td align="center">O(1)</td>
</tr>
<tr>
<td align="center">addFirst(ele)</td>
<td align="center">O(n)</td>
</tr>
<tr>
<td align="center">add(index, ele)</td>
<td align="center">O(n/2) = O(n)</td>
</tr>
<tr>
<td align="center">resize(newCapacity)</td>
<td align="center">O(n)</td>
</tr>
</tbody>
</table>
<p>其实,在执行<code>addLast</code>的时候,我们并不是每次都会触发<code>resize</code>方法,更多的时候,复杂度只是<code>O(1)</code>而已。<br><strong>比方说:</strong><br>当前的<code>capacity = 8</code>,并且每一次添加操作都使用<code>addLast</code>,第9次<code>addLast</code>操作,触发<code>resize</code>,总共17次基本操作(<code>resize</code>方法会进行8次操作,<code>addLast</code>方法进行9次操作)。平均,每次<code>addLast</code>操作,进行2次基本操作(17 / 9 ≈ 2)。<br><strong>假设:</strong><br><code>capacity = n</code>, <code>n + 1</code>次<code>addLast</code>,触发<code>resize</code>,总共进行了<code>2n + 1</code>次操作,平均每次<code>addLast</code>操作,进行了2次基本操作。</p>
<p><strong>这样均摊计算,时间复杂度是O(1)!</strong></p>
<h4>(2)防止复杂度的震荡</h4>
<blockquote>让我们来假设这样一种情况:<br>当<code>size == data.length</code>时,我们执行了<code>addLast</code>方法添加一个元素,这个时候我们需要去执行<code>resize</code>方法,此时,<code>addLast</code>的复杂度为<code>O(n)</code>。<br>然后,我去<code>removeLast</code>,此时的<code>removeLast</code>复杂度也是<code>O(n)</code>。<br>再然后,我再去执行<code>addLast</code>。<br>.<br>.<br>.</blockquote>
<p>有没有发现,在这样一种极端情况下,<code>addLast</code>和<code>removeLast</code>的复杂度变成了<code>O(n)</code>,其实,这个就是<strong>复杂度的震荡</strong>。</p>
<ul>
<li>
<p>为什么我们会产生这种震荡?</p>
<ul><li>
<code>add</code>情况下,我们去扩容数组无可厚非。但是<code>remove</code>情况下,我们立刻去缩容数组就有点不合适了。</li></ul>
</li>
<li>
<p>怎么去解决这种情况?</p>
<ul>
<li>因为我们之前采取的措施是<code>Eager</code>
</li>
<li>所以,我们采取一种<code>Lazy</code>的方式:当<code>size == data.length / 2</code>,我们不要立刻缩容,当<code>size == data.length / 4</code>时,我们才去缩容,就可以很好的解决这种震荡。</li>
</ul>
</li>
</ul>
<blockquote>具体代码如下,其实只是对<code>remove</code>进行了极小的改变</blockquote>
public E remove(int index) {
if (index &lt; 0 || index &gt;= size) {
throw new IllegalArgumentException("remove failed. index is illegal");
} E result = data[index];
for (int i = index + 1; i &lt; size; i++) {
data[i - 1] = data[i];
}
// 空间释放,垃圾回收会自动回收
data[--size] = null; // 减小数组长度,不要浪费空间,防止震荡
if (size == data.length / 4 &amp;&amp; data.length / 2 != 0) {
resize(data.length / 2);
} return result;

}



原文地址:https://segmentfault.com/a/1190000016064569

我理解的数据结构(一)—— 数组(Array)的更多相关文章

  1. 数据结构之数组Array

    数组Array 基本操作 Status InitArray(int dimm,...)//若维数dim和随后的各维长度合法,则构造相应的数组A,并返回OK Status DestroyArray() ...

  2. 算法与数据结构基础 - 数组(Array)

    数组基础 数组是最基础的数据结构,特点是O(1)时间读取任意下标元素,经常应用于排序(Sort).双指针(Two Pointers).二分查找(Binary Search).动态规划(DP)等算法.顺 ...

  3. 表示集合的数据结构:数组(Array),对象(Object),Map和Set

    Map和Set是ES6标准新增的数据类型 Map: 是一组键值对的结构,使用一个二维数组来初始化Map,例如: var m = new Map([['xiaohong',100],['xiaolan' ...

  4. 我理解的数据结构(二)—— 栈(Stack)

    我理解的数据结构(二)-- 栈(Stack) 一.栈基础 栈是一种线性结构 相比较数组,栈对应的操作是数组的子集 只能从一端添加元素,也只能从同一端取出元素,这一端称为栈顶 栈是一种后进先出的数据结构 ...

  5. 我理解的数据结构(三)—— 队列(Queue)

    我理解的数据结构(三)-- 队列(Queue) 一.队列 队列是一种线性结构 相比数组,队列对应的操作是数组的子集 只能从一端(队尾)添加元素,只能从另一端(队首)取出元素 队列是一种先进先出的数据结 ...

  6. Java-杂项:Java数组Array和集合List、Set、Map

    ylbtech-Java-杂项:Java数组Array和集合List.Set.Map 1.返回顶部 1. 之前一直分不清楚java中的array,list.同时对set,map,list的用法彻底迷糊 ...

  7. javascript数组array

    注意:1.array的length不是只读的.可以从数组的末尾移出项或者向数组中添加新项.看下面例子: var colors = ["red","yellow" ...

  8. Java ArrayList和Vector、LinkedList与ArrayList、数组(Array)和列表集合(ArrayList)的区别

    ArrayList和Vector的区别ArrayList与Vector主要从二方面来说.  一.同步性:   Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步 ...

  9. go 数组(array)、切片(slice)、map、结构体(struct)

    一 数组(array) go语言中的数组是固定长度的.使用前必须指定数组长度. go语言中数组是值类型.如果将数组赋值给另一个数组或者方法中参数使用都是复制一份,方法中使用可以使用指针传递地址. 声明 ...

随机推荐

  1. cocos2d的armature绑定到其它armature骨骼上的bug

    在cocos2dx中,rmature的骨骼上能够绑定另外的armature,在我的项目中使用了该功能来完毕骑乘功能,可是在使用过程发现了例如以下的bug,特写在这里做一下记录. </span&g ...

  2. linux驱动之设备号与创建设备节点

    设备号: 1.自己主动分配 major = register_chrdev(0,"first_drv",&first_sdv_fops);//注冊 注冊设备时给设备号写0, ...

  3. Android-通过SlidingPaneLayout高仿微信6.2最新版手势滑动返回(一)

    近期更新了微信版本号到6.2.发现里面有个很好的体验,就是在第二个页面Activity能手势向右滑动返回,在手势滑动的过程中能看到第一个页面,这样的体验很赞,这里高仿了一下. 这里使用的是v4包里面的 ...

  4. 排列(permutation) 用1,2,3,…,9组成3个三位数abc,def和ghi,每个数字恰好使用一次,要 求abc:def:ghi=1:2:3。按照“abc def ghi”的格式输出所有解,每行一个解。

    #include <stdio.h> #include <math.h> // 算法竞赛的目标是编程对任意输入均得到正确的结果. // 请先独立完成,如果有困难可以翻阅本书代码 ...

  5. [SCOI 2010] 连续攻击游戏

    [题目链接] https://www.luogu.org/problemnew/show/P1640 [算法] 二分图匹配 实现时需要常数优化和特判 [代码] //code by byf and lm ...

  6. 【HAOI 2008】 糖果传递

    [题目链接] https://www.lydsy.com/JudgeOnline/problem.php?id=1045 [算法] 环形均分纸牌问题 [代码] #include<bits/std ...

  7. [Swift通天遁地]二、表格表单-(5)实现表格下拉和上拉刷新效果

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...

  8. 2019 年了,为什么我还在用 jQuery?

    译者按: 看来 jQuery 还是有一些用武之地的. 原文: Why I'm Still Using jQuery in 2019 译者: Fundebug 为了保证可读性,本文采用意译而非直译.翻译 ...

  9. ACM_小凯的排序(字符串)

    小凯的排序 Time Limit: 2000/1000ms (Java/Others) Problem Description: 调皮的小凯喜欢排序,拿到什么东西都要排一下序.现在他觉得单一的递增递减 ...

  10. [转]在 Linux 下使用 RAID

    转自:http://www.linuxidc.com/Linux/2015-08/122191.htm RAID 的意思是廉价磁盘冗余阵列(Redundant Array of Inexpensive ...