数据结构与算法之数组

(一) JAVA中的数组

1.定义

数组属于引用数据类型, 用来存储固定大小的同类型元素的容器, 数据类型可以是8种基本的数据类型, 也可以是引用数据类型…

2.两种初始化方式
  • 动态初始化: 声明并开辟指定长度的数组, 会为数组之中的每个元素附上该数据类型的默认值
    (byte,short,int,long: 0; float,double: 0.0; boolean: false; char: ‘\u0000’; 引用类型: null)
	数据类型[] 数组名称 = new 数据类型[数组长度];
	int[] arr = new int[3];
  • 静态初始化: 声明数组同时指定其内容元素.
	数据类型[] 数组名称 = new 数据类型[]{...元素};
	String[] students = new String[] {"张三", "李四"};
	
	数据类型[] 数组名称 = {...元素};
	int[] grades = {1, 2, 3, 4};
3.遍历

存储到数组中的每个元素,都有属于自己的自动编号(索引), 因此通过索引来访问数组中的元素(快速查询).

	for (int i = 0; i < grades.length; i++) {
		System.out.println(grades[i]);
	}

数组的优点:
    1. 按照索引查询元素速度快
    2. 能存储大量数据
    3. 按照索引遍历数组方便
 
数组中存在的问题:
    1. 数组的大小一经确定不能改变
    2. 数组只能存储一种类型的数据
    3. 如何添加元素, 删除元素? 当添加一个元素时, 数组大小已满等等问题…
 
因此对数组进行了二次封装

(二) 二次封装数组

1. 新建二次封装数组实体类Array
	/**
	 * 二次封装数组实体
	 * 
	 * @author Administrator
	 *
	 */
	public class Array {
	
		/**
		 * 存储数据的源数组
		 */
		private int[] data;
	
		/**
		 * 描述data源数组有效元素的个数
		 */
		private int size;
	
		/**
		 * 构造函数, 传入数组的容量capacity构造Array
		 * 
		 * @param capacity
		 */
		public Array(int capacity) {
			data = new int[capacity];
			size = 0;
		}
	
		/**
		 * 无参数的构造函数, 默认数组的容量capacity=10
		 */
		public Array() {
			this(10);
		}
	
		/**
		 * 获取数组中的有效元素个数
		 * 
		 * @return
		 */
		public int getSize() {
			return size;
		}
	
		/**
		 * 获取数组的容量
		 * 
		 * @return
		 */
		public int getCapacity() {
			return data.length;
		}
		
		/**
		 * 返回数组中有效元素是否为空
		 * 
		 * @return
		 */
		public boolean isEmpty() {
			return size == 0;
		}
	
	}
2. 二次封装数组的增加操作
  • 向所有元素最后添加一个元素e
	public void addLast(int e) {
		// 二次封装数组的有效元素的个数等于源数组的长度时, 抛出非法参数异常(待优化)
		if(size == data.length) {
			throw new IllegalArgumentException("addList() failed. Array is full.");
		}
		data[size] = e;
		size++;
	}
  • 在第index个位置插入一个新元素e
	public void add(int index, int e) {
		// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常
		if (size == data.length) {
			throw new IllegalArgumentException("add() failed. Array is full.");
		}

		/*
		 * index索引必须大于0, 且小于或等于数组有效元素的个数, 为了保证数组数据在一组连续的内存空间紧密排列 当索引等于有效元素的个数时,
		 * 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
		 */
		if (index < 0 || index > size) {
			throw new IllegalArgumentException("add() failed. Require index >= 0 and index <= size");
		}

		// 把 最后一个元素 至 index索引处的元素 向后移一个位置. 从最后一个元素开始依次移动, 防止元素发生覆盖
		for (int i = size - 1; i >= index; i--) {
			data[i + 1] = data[i];
		}
		printData();
		data[index] = e;
		size++;
	}
  • 优化addLast()方法: 在add()方法中, 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
	public void addLast(int e) {	
		// 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
		add(size, e);
	}
  • 同理, 在所有元素前添加一个新元素
	public void addFirst(int e) {
		add(0, e);
	}
3. 二次封装数组的查询操作
  • 覆盖重写toString()方法, 遍历查询所有的元素
	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
		res.append("[");
		for (int i = 0; i < size; i++) {
			res.append(data[i]);
			if(i != size - 1) {
				res.append(", ");
			}
		}
		res.append("]");
		return res.toString();
	}

测试:

	public static void main(String[] args) {
		Array array = new Array(20);
		for (int i = 0; i < 10; i++) {
			array.addLast(i);
		}
		System.out.println(array);
		array.add(1, 100);
		System.out.println(array);
		array.addFirst(-1);
		System.out.println(array);
	}

在这里插入图片描述

  • 获取index索引位置的元素
	public int get(int index) {
		// 判断索引的合法性, 且保证无法查询未使用空间的元素
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal.");
		}
		return data[index];
	}
  • 获取第一个元素
	public E getFirst() {
		return get(0);
	}
  • 获取最后一个元素
	public E getFirst() {
		return get(size - 1);
	}
4. 二次封装数组的修改操作
  • 修改index索引位置的元素
	public void set(int index, int e) {
		// 判断索引的合法性, 且保证无法查询未使用空间的元素
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal.");
		}
		data[index] = e;
	}
5. 二次封装数组的删除操作
  • 从数组中删除index位置的元素, 返回删除的元素
	public int remove(int index) {
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal."); 
		}
		int res = data[index];
		/*
		 * 把 index+1索引处 至 最后一个元素 向前移一个位置, 且数组的有效个数size减一: 此操作会导致data[size]存在值且data[size]与data[size-1]的值相同
		 * 	1. 由于getget(int index)的方法, 在获取指定索引处元素时会判断索引的合法性, 且保证无法查询未使用空间的元素
		 *  2. 居于数组的特性: 数组之中的每个元素附上该数据类型的默认值, 因此无论是默认值还是data[size-1]的值都可以
		 *  
		 *  所以这一点无关大雅...
		 */
		for (int i = index + 1; i < size; i++) {
			data[i - 1] = data[i];
		}
		size--;
		return res;
	}
  • 从数组中删除第一个元素, 返回删除的元素
	public int removeFirst() {
		return remove(0);
	}
  • 从数组中删除最后一个元素, 返回删除的元素
	public int removeLast() {
		return remove(size-1);
	}
  • 从数组中删除元素e
	public void removeElement(int e) {
		int index = find(e);
		if(index != -1) {
			remove(index);
		}
	}
  • 扩展: 采用递归的方式, 从数组中删除所有元素e
	public void removeAllElement(int e) {
		int index = find(e);
		if(index != -1) {
			remove(index);
			removeAllElement(e);
		} else {
			return;
		}
	}
6. 二次封装数组的包含、搜索操作
  • 查找数组中是否存在元素e
	public boolean contain(int e) {
		for (int i = 0; i < size; i++) {
			if(data[i] == e) {
				return true;
			}
		}
		return false;
	}
  • 查找数组中元素e所在的索引, 如果不存在元素e, 则返回-1
	public int find(int e) {
		for (int i = 0; i < size; i++) {
			if(data[i] == e) {
				return i;
			}
		}
		return -1;
	}

二次封装数组中存在的问题值之一: 数组作为存储数据的容器, 不能只存储一种类型的变量
 
因此对二次封装数组引入泛型

(三) 二次封装数组引入泛型

引入泛型的好处和注意事项

好处: 让二次封装数组(数据结构)可以放置任何的引用类型数据
 
注意:

  1. 在有参构造函数中动态初始化泛型数组, 不存在语法:
    data = new E[capaticy];
    需要借助 所有类型的父类Object, 然后进行强制转换(向下转换)
    data = (E[]) new Object[capacity];
  2. 引入泛型后, 存储的都是引用类型. 引用类型的比较使用 equals()方法, 且需要重写hashCode() 和 equals() 方法.
public class Array<E> {

	/**
	 * 存储数据的源数组
	 */
	private E[] data;

	/**
	 * 描述data源数组有效元素的个数
	 */
	private int size;

	/**
	 * 构造函数, 传入数组的容量capacity构造Array
	 * 
	 * @param capacity
	 */
	@SuppressWarnings("unchecked")
	public Array(int capacity) {
		data = (E[]) new Object[capacity];
		size = 0;
	}

	/**
	 * 无参数的构造函数, 默认数组的容量capacity=10
	 */
	public Array() {
		this(10);
	}

	/**
	 * 获取数组中的有效元素个数
	 * 
	 * @return
	 */
	public int getSize() {
		return size;
	}

	/**
	 * 获取数组的容量
	 * 
	 * @return
	 */
	public int getCapacity() {
		return data.length;
	}

	/**
	 * 返回数组中有效元素是否为空
	 * 
	 * @return
	 */
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * 向所有元素最后添加一个元素e
	 * 
	 * @param e
	 */
	public void addLast(E e) {
//		// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常(待优化)
//		if (size == data.length) {
//			throw new IllegalArgumentException("addList() failed. Array is full.");
//		}
//		data[size] = e;
//		size++;
		
		// 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
		add(size, e);
	}

	/**
	 * 在第index个位置插入一个新元素e: 把 最后一个元素 至 index索引处的元素 向后移一个位置. 从最后一个元素开始依次移动, 防止元素发生覆盖
	 * 
	 * @param index
	 * @param e
	 */
	public void add(int index, E e) {
		// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 抛出非法参数异常
		if (size == data.length) {
			throw new IllegalArgumentException("add() failed. Array is full.");
		}

		/*
		 * index索引必须大于0, 且小于或等于数组有效元素的个数, 为了保证数组数据在一组连续的内存空间紧密排列 当索引等于有效元素的个数时,
		 * 当index索引等于size有效元素的个数, 相当于向数组最后添加一个元素
		 */
		if (index < 0 || index > size) {
			throw new IllegalArgumentException("add() failed. Require index >= 0 and index <= size");
		}

		// 把 最后一个元素 至 index索引处的元素 向后移一个位置. 从最后一个元素开始依次移动, 防止元素发生覆盖
		for (int i = size - 1; i >= index; i--) {
			data[i + 1] = data[i];
		}
		data[index] = e;
		size++;
	}
	
	/**
	 * 在所有元素前添加一个新元素
	 * 
	 * @param e
	 */
	public void addFirst(E e) {
		add(0, e);
	}
	
	/**
	 * 获取index索引位置的元素
	 * 
	 * @param index
	 * @return
	 */
	public E get(int index) {
		// 判断索引的合法性, 且保证无法查询未使用空间的元素
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal.");
		}
		return data[index];
	}
	
	/**
	 * 修改index索引位置的元素
	 * 
	 * @param index
	 * @param e
	 */
	public void set(int index, E e) {
		// 判断索引的合法性, 且保证无法查询未使用空间的元素
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal.");
		}
		data[index] = e;
	}
	
	/**
	 * 查找数组中是否存在元素e
	 * 
	 * @param e
	 * @return
	 */
	public boolean contain(E e) {
		for (int i = 0; i < size; i++) {
			if(data[i].equals(e)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 查找数组中元素e所在的索引, 如果不存在元素e, 则返回-1
	 * 
	 * @param e
	 * @return
	 */
	public int find(E e) {
		for (int i = 0; i < size; i++) {
			if(data[i] == e) {
				return i;
			}
		}
		return -1;
	}
	
	// 从数组中删除index位置的元素, 返回删除的元素
	public E remove(int index) {
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal."); 
		}
		E res = data[index];
		
		/*
		 * 把 index+1索引处 至 最后一个元素 向前移一个位置, 且数组的有效个数size减一: 此操作会导致data[size]存在值且data[size]与data[size-1]的值相同
		 * 	1. 由于getget(int index)的方法, 在获取指定索引处元素时会判断索引的合法性, 且保证无法查询未使用空间的元素
		 *  2. 居于数组的特性: 数组之中的每个元素附上该数据类型的默认值, 因此无论是默认值还是data[size-1]的值都可以
		 *  
		 *  所以这一点无关大雅...
		 */
		for (int i = index + 1; i < size; i++) {
			data[i - 1] = data[i];
		}
		size--;
		return res;
	}
	
	/**
	 * 从数组中删除第一个元素, 返回删除的元素
	 *  
	 * @return
	 */
	public E removeFirst() {
		return remove(0);
	}
	
	/**
	 * 从数组中删除最后一个元素, 返回删除的元素
	 *  
	 * @return
	 */
	public E removeLast() {
		return remove(size-1);
	}
	
	/**
	 * 从数组中删除元素e
	 * 
	 * @param e
	 */
	public void removeElement(E e) {
		int index = find(e);
		if(index != -1) {
			remove(index);
		}
	}
	
	/**
	 * 从数组中删除所有元素e
	 * 
	 * @param e
	 */
	public void removeAllElement(E e) {
		int index = find(e);
		if(index != -1) {
			remove(index);
			removeAllElement(e);
		} else {
			return;
		}
	}
	
	/**
	 * 覆盖重写toString()方法, 遍历查询所有的元素
	 */
	@Override
	public String toString() {
		StringBuilder res = new StringBuilder();
		res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length));
		res.append("[");
		for (int i = 0; i < size; i++) {
			res.append(data[i]);
			if(i != size - 1) {
				res.append(", ");
			}
		}
		res.append("]");
		return res.toString();
	}
}

二次封装数组中存在的问题值之二: 数组作为存储数据的容器, 无法确定容器的空间大小
 
因此对数组封装动态数组

(四) 动态数组

1.优化add()方法

当数组容量已满时, 让二次封装数组类拥有容量扩容的能力

	public void add(int index, E e) {
		if (index < 0 || index > size) {
			throw new IllegalArgumentException("add() failed. Require index >= 0 and index <= size");
		}
		
		// 二次封装数组的有效元素的个数等于源数组的长度时, 数组容量已满, 扩容元素组容量的2倍
		if (size == data.length) {
			resize(2 * data.length);
		}
		for (int i = size - 1; i >= index; i--) {
			data[i + 1] = data[i];
		}
		data[index] = e;
		size++;
	}
	
	/**
	 * 数组扩容, 新建一个容量为newCapacity的数组赋值给data
	 * @param capacity
	 */
	private void resize(int newCapacity) {
		@SuppressWarnings("unchecked")
		E[] newData = (E[]) new Object[newCapacity];
		for(int i = 0; i < size; i++) {
			newData[i] = data[i];
		}
		data = newData;
	}

测试

	public static void main(String[] args) {
		// 空的构造方法默认数组初始容量为 10
		Array<Integer> array = new Array<>();
		
		// for循环为数组赋值 0-9
		for (int i = 0; i < 10; i++) {
			array.addLast(i);
		}
		System.out.println(array);
		
		// 数组最后添加一个元素, 数组容量已满,  扩容
		array.addLast(10);
		System.out.println(array);
	}

数组的容量capacity由原来的10变成20
在这里插入图片描述

2.优化remove()方法

当数组有效元素的个数较少时, 让二次封装数组类拥有容量缩容的能力

	public E remove(int index) {
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal."); 
		}
		E res = data[index];
		for (int i = index + 1; i < size; i++) {
			data[i - 1] = data[i];
		}
		size--;
		
		// 数组有效元素个数 等于 数组容量的一半, 缩容至原容量的二分之一
		if(size == data.length / 2) {
			resize(data.length / 2);
		}
		return res;
	}

测试

	public static void main(String[] args) {
		Array<Integer> array = new Array<>(20);

		// for循环为数组赋值 0-10, 数组有效元素个数为 11
		for (int i = 0; i < 11; i++) {
			array.addLast(i);
		}
		System.out.println(array);

		// 数组删除最后一个元素, 数组有效元素个数为 10 等于数组容量的一半, 缩容至原容量的二分之一
		array.removeLast();
		System.out.println(array);
	}

数组的容量capacity由原来的20变成10
在这里插入图片描述

(五) 时间复杂度分析

1.概念
  • O(1), O(n), O(lgn), O(nlogn), O(n^2)
  • O: 描述的是算法的运行时间和输入数据之间的关系.
	public static int sum(int[] nums) {
		int sum = 0;
		for(int num : nums) {
			sum += num;
		}	
		return sum;
	}

在上面的求和函数中, 它的时间复杂度为O(n). n是nums中的元素个数, 函数的运行时间取决于nums中的元素个数, 算法和n呈线性关系. 为什么叫O(n)呢? 是因为呈线性关系的表达式为: T = c1*n + c2, 在此忽略了常数(常数受运行环境, 编码习惯, 硬件等影响无法得出具体的值而忽略).
c1 = 循环取出num的时间 + 求和时间; c2 = 初始化变量sum的时间 + 函数返回结果的时间

a: T = 2*n + 2        O(n)

b: T = 2000*n + 10000    O(n)

c: T = 1nn + 0          O(n^2)
 
在上面3个算法中, c算法的性能会比a和b算法的性能差, 应为c算法是平方级别的算法.但这有同学会问这取决于n的大小, 这就引出了O的定义: 渐进时间复杂度

O的定义: 渐进时间复杂度, 描述n趋近于无穷的情况下时间的复杂度

2.分析动态数组的时间复杂度
  • 添加操作: 整体时间复杂度为 O(n)
函数时间复杂度分析
addLast(e)O(1)直接往size索引处赋值, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成
addFirst(e)O(n)数组头部添加, 所有元素向后移一个单位, 与数据规模呈线性关系
add(index, e)O(n/2) = O(n)消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略
总结:O(n)添加操作整体时间复杂度为 O(n), 分析时间复杂度一般考虑最坏的情况,同时还要考虑数组扩容resize() -> O(n)
  • 删除操作: 整体时间复杂度为 O(n)
函数时间复杂度分析
removeLast(e)O(1)直接size–, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成
removeFirst(e)O(n)删除数组头部, 所有元素向前移一个单位, 与数据规模呈线性关系
remove(index, e)O(n/2) = O(n)消耗的时间与index的取值有关,范围是 O(1)–O(n), 涉及到概率论知识.平均取值n/2, 2是常数省略
总结:O(n)删除操作整体时间复杂度为 O(n),同时还要考虑数组缩容resize() -> O(n)
  • 查询操作: 已知索引 O(1); 未知索引 O(n)
函数时间复杂度分析
get(index)O(1)直接通过索引访问, O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成
contain(e)O(n)需要遍历素组, 查找元素
find(e)O(n)需要遍历素组, 查找元素
总结:O(n)已知索引 O(1); 未知索引 O(n)
  • 修改操作: 时间复杂度为 O(1)
函数时间复杂度分析
set(index, e)O(1)直接通过索引修改. O(1): 代表此操作消耗的时间与数据规模无关系的, 在常数时间内完成
总结:O(1)修改操作整体时间复杂度为 O(1)
3.防止复杂度震荡

在动态数组中的addLast和removerLast操作中存在这样的问题: 当有个capacity =n容量已满的数组, 进行addLast()操作, 势必会发生扩容resize操作(O(n)), 容量为2n. 添加操作完成后, 立马执行removerLast操作, 删除该元素后, 此时, 元素个数是容量的二分之一, 势必也会发生缩容resize操作(O(n)), 如果两次操作循环执行, 每次都会触发resize操作, 从而发生了复杂度震荡.

出现问题的原因: removerLast 时 resize 过于着急(Eager)
解决方案: 采用Lazy懒惰策略, 当数组有效元素size 等于 容量capacity的四分之一时, 才缩容二分之一, 此时, 再有添加操作时, 也无需扩容.

优化remover()方法

	public E remove(int index) {
		if(index < 0 || index >= size) {
			throw new IllegalArgumentException("get() failed. Index is illegal."); 
		}
		E res = data[index];
		for (int i = index + 1; i < size; i++) {
			data[i - 1] = data[i];
		}
		size--;
		
		// 数组有效元素个数 等于 数组容量的四分之一, 缩容至原容量的二分之一. 防止复杂度震荡
		if(size == data.length / 4 && data.length / 2 != 0) {
			resize(data.length / 2);
		}
		return res;
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值