Principle and implementation of dynamic expansion array

Posted by kindoman on Thu, 09 Dec 2021 11:41:02 +0100

Principle and implementation of dynamic expansion array

What is a data structure?

Data structure is a way for computer to store and organize data;

Linear table

A linear table is a finite sequence with n elements of the same type (n ≥ 0)

  • a1 is the first node (first element) and an is the tail node (tail element)
  • a1 is the precursor of a2 and a2 is the successor of a1

Common linear tables are:

  • array
  • Linked list
  • Stack
  • queue
  • Hash table (hash table)

array

An array is a linear table stored sequentially, and the memory addresses of all elements are continuous

In many programming languages, arrays have a fatal disadvantage: they cannot dynamically modify capacity

In the actual development, we hope that the capacity of the array can be changed dynamically

Dynamic Array

Design of dynamic array interface

Structure of dynamic array:

  • size
  • elements

In Java, member variables are automatically initialized, such as:

  • int type is automatically initialized to 0
  • The object type is automatically initialized to null

Clear all elements - clear()

The core code is size = 0. In fact, there is no need to perform the operations of elements = null and size = 0, which ensures that users cannot access the elements in the dynamic array.

Add element - add(E element), add(int index, E element)

add(E element): add elements to the last array by default;

add(int index, E element): insert an element at the index position;

For example, to add an element where index = 2:

The correct order should be: move the elements after index = 2 back from back to front, and then assign values:

If you start moving elements from front to back, the following error results:

Code implementation:

/**
 * Insert an element at the index position
 * @param index
 * @param element
 */
public void add(int index, E element){ 
	rangeCheckForAdd(index); // Check subscript out of bounds
	ensureCapacity(size + 1); // Make sure the capacity is large enough
	// 0 1 2 3 4 5 6 7 8 9	(index)
	// 1 2 3 4 5 6 x x x x 	 (original array)
	// At index=2, insert 9 and move all elements back
	// 1 2 9 3 4 5 6 x x x 	 (array after add)
	// Start from back to front, move each element back one bit, and then assign a value
	for (int i = size - 1; i > index; i--) {
		elements[i + 1] = elements[i];
	}
	elements[index] = element; // assignment
	size++;
}
/**
 * Add elements to the end of the array
 */
public void add(E element){
	add(size, element);
}

Delete element - remove(int index), empty array - clear()

For example, to delete an array element with index = 3, you should start moving from front to back and overwrite the previous element with the latter element.

Thinking: how to deal with the last element?

If the int type is stored, the last element cannot be accessed after size -.
If you use generic arrays, pay attention to memory management (set elements to null).
Using generic technology can make dynamic arrays more general and can store any data type:

Code implementation:

/**
 * Delete element at index position
 * @param index
 * @return
 */
public E remove(int index){
	rangeCheck(index);
	// 0 1 2 3 4 5 	(index)
	// 1 2 3 4 5 6  	 (original array)
	// Delete the element with index 2 and move the element forward
	// 1 2 4 5 6 	 (array after remove)
	// Move from front to back, covering the front element with the rear element
	E old = elements[index];
	for (int i = index; i < size - 1; i++) {
		elements[i] = elements[i + 1];
	}
	// The following is what you need to write after using generics (not if you store int data)
	elements[--size] = null; // After deleting the element, set the last bit to null
	return old;
}
/**
 * Clear all elements
 */
public void clear(){
	// Pay attention to memory management after using generic arrays (set elements to null)
	for (int i = 0; i < size; i++) {
		elements[i] = null;
	}
	size = 0;
}

Include an element - contains(E element)

The processing of null mainly depends on your business requirements: can null data be stored?

/**
 * Include an element
 * @param element
 * @return
 */
public boolean contains(E element){
	return indexOf(element) != ELEMENT_NOT_FOUND; // Returns True if the element is not found
}
/**
* View the index of the element
 * @param element
 * @return
 */
public int indexOf(E element){
	/*
	// It's OK not to handle null, but it's not robust enough
	for (int i = 0; i < size; i++) {
		if(elements[i].equals(element)) return i;
	}
	 */
	if(element == null){ // Process null
		for (int i = 0; i < size; i++) {
			if(elements[i] == null) return i;
		}
	}else{
		for (int i = 0; i < size; i++) {
			if(elements[i].equals(element)) return i;
		}
	}
	return ELEMENT_NOT_FOUND;
}

Capacity expansion - ensureCapacity(int capacity)

I believe the little friends who have seen the video will understand it as soon as they see this picture.

Implementation of capacity expansion operation code:

/**
 * Capacity expansion operation
 */
private void ensureCapacity(int capacity){
	int oldCapacity = elements.length;
	if (oldCapacity >= capacity) return;
	// The new capacity is 1.5 times the old capacity
	int newCapacity = oldCapacity + (oldCapacity >> 1);
	E[] newElements = (E[])new Object[newCapacity];
	for (int i = 0; i < size; i++) {
		newElements[i] = elements[i]; // Copy the original array elements to the new array
	}
	elements = newElements;
	System.out.println("size="+oldCapacity+", Expansion to"+newCapacity);
}

Print array (toString)

  • Override toString method
  • Concatenate elements into strings in the toString method
  • StringBuilder is recommended for string splicing
@Override
public String toString() {
	// Print form: size=5, [99, 88, 77, 66, 55]
	StringBuilder string = new StringBuilder();
	string.append("size=").append(size).append(", [");
	for (int i = 0; i < size; i++) {
		if(0 != i) string.append(", ");
		string.append(elements[i]);
	}
	string.append("]");
	return string.toString();
}

generic paradigm

Using generic technology can make dynamic arrays more general and can store any data type

public class ArrayList<E> {
	private int size;
	private E[] elements;
}
elements = (E[]) new Object[capacity];

Object array - object []

Object[] objects = new Object[7];

Because objects can store any type, it is impossible to fix the memory space occupied by objects (the space occupied is different according to different user-defined objects passed in). Therefore, in fact, the address of the Object is stored in the Object array.
To destroy an object, you only need to assign the address pointing to the object to null. If there is no address to reference the object, it will be automatically garbage collected

Memory management details:

Empty array clear() delete element clear()

public void clear() {
	for (int i = 0; i < size; i++) {
		elements[i] = null; // Manage memory
	}
	size = 0;
}
public E remove(int index) {
	rangeCheck(index);
	E oldElement = elements[index];
	for (int i = index; i < size - 1; i++) {
		elements[i] = elements[i + 1];
	}
	elements[--size] = null; // Manage memory
	return oldElement;
}

int dynamic array source code (Java)

public class ArrayList {
	private int size;		// Number of elements	
	private int[] elements; // All elements

	private static final int DEFAULT_CAPACITY = 10; // Initial capacity
	private static final int ELEMENT_NOT_FOUND = -1;
	
	public ArrayList(int capacity) { // If the capacity is less than 10, it will be expanded to 10
		capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
		elements = new int[capacity];
	}
	public ArrayList(){
		this(DEFAULT_CAPACITY);
	}
	/**
	 * Number of elements
	 * @return
	 */
	public int size(){
		return size;
	}
	/**
	 * Is it empty
	 * @return
	 */
	public boolean isEmpty(){
		return size == 0;
	}
	/**
	 * Include an element
	 * @param element
	 * @return
	 */
	public boolean contains(int element){
		return indexOf(element) != ELEMENT_NOT_FOUND; //Returns True if the element is not found
	}
	/**
	 * Insert an element at the index position
	 * @param index
	 * @param element
	 */
	public void add(int index, int element){
		rangeCheckForAdd(index); // Check subscript out of bounds
		ensureCapacity(size + 1); // Make sure the capacity is large enough
		
		// 0 1 2 3 4 5 6 7 8 9	(index)
		// 1 2 3 4 5 6 x x x x 	 (original array)
		// At index=2, insert 9 and move all elements back
		// 1 2 9 3 4 5 6 x x x 	 (array after add)
		// Start from back to front, move each element back one bit, and then assign a value
		for (int i = size - 1; i > index; i--) {
			elements[i + 1] = elements[i];
		}
		elements[index] = element; // assignment
		size++;
	}
	/**
	 * Add element to last
	 */
	public void add(int element){
		add(size, element);
	}
	/**
	 * Element that sets the index location
	 * @param index
	 * @param element
	 * @return Original element ֵ
	 */
	public int get(int index){
		rangeCheck(index);
		return elements[index];
	}
	/**
	 * Element that sets the index location
	 * @param index
	 * @param element
	 * @return Original element ֵ
	 */
	public int set(int index, int element){
		rangeCheck(index);
		int old = elements[index];
		elements[index] = element;
		return old;
	}
	/**
	 * Delete element at index position
	 * @param index
	 * @return
	 */
	public int remove(int index){
		rangeCheck(index);
		
		// 0 1 2 3 4 5 	(index)
		// 1 2 3 4 5 6  	 (original array)
		// Delete the element with index 2 and move the element forward
		// 1 2 4 5 6 	 (array after remove)
		int old = elements[index];
		// Move from front to back, covering the front element with the rear element
		for (int i = index; i < size-1; i++) {
			elements[i] = elements[i + 1];
		}
		size--;
		return old;
	}
	/**
	 * View the index of the element
	 * @param element
	 * @return
	 */
	public int indexOf(int element){
		for (int i = 0; i < size; i++) {
			if(elements[i] == element) return i;
		}
		return ELEMENT_NOT_FOUND;
	}
	/**
	 * Clear all elements
	 */
	public void clear(){
		size = 0;
	}
	/*
	 * Capacity expansion operation
	 */
	private void ensureCapacity(int capacity){
		int oldCapacity = elements.length;
		if(oldCapacity >= capacity) return;
		// The new capacity is 1.5 times the old capacity
		int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5
		int[] newElements = new int[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[i];
		}
		elements = newElements;
		System.out.println("size="+oldCapacity+", Expansion to"+newCapacity);
	}
	/****************Encapsulated function*******************************/
	// Exception thrown when subscript is out of bounds
	private void outOfBounds(int index) {
		throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
	}
	// Check for subscript out of bounds (inaccessible or deleted location)
	private void rangeCheck(int index){
		if(index < 0 || index >= size){
			outOfBounds(index);
		}
	}
	// Check that the subscript of add() is out of bounds (you can add it at the size position)
	private void rangeCheckForAdd(int index) {
		if (index < 0 || index > size) {
			outOfBounds(index);
		}
	}
	/****************Encapsulated function*******************************/
	@Override
	public String toString() {
		// Print form: size=5, [99, 88, 77, 66, 55]
		StringBuilder string = new StringBuilder();
		string.append("size=").append(size).append(", [");
		for (int i = 0; i < size; i++) {
			if(0 != i) string.append(", ");
			string.append(elements[i]);
		}
		string.append("]");
		return string.toString();
	}
}

Generic dynamic array source code (Java)

@SuppressWarnings("unchecked")
public class ArrayList<E> {
	private int size;		// Number of elements	
	private E[] elements; 	// All elements

	private static final int DEFAULT_CAPACITY = 10; // Initial capacity
	private static final int ELEMENT_NOT_FOUND = -1;
	
	public ArrayList(int capacity) { // If the capacity is less than 10, it will be expanded to 10
		capacity = (capacity < DEFAULT_CAPACITY) ? DEFAULT_CAPACITY : capacity;
		elements = (E[])new Object[capacity];
	}
	public ArrayList(){
		this(DEFAULT_CAPACITY);
	}
	/**
	 * Number of elements
	 * @return
	 */
	public int size(){
		return size;
	}
	/**
	 * Is it empty
	 * @return
	 */
	public boolean isEmpty(){
		return size == 0;
	}
	/**
	 * Include an element
	 * @param element
	 * @return
	 */
	public boolean contains(E element){
		return indexOf(element) != ELEMENT_NOT_FOUND; // Returns True if the element is not found
	}
	/**
	 * Insert an element at the index position
	 * @param index
	 * @param element
	 */
	public void add(int index, E element){ 
		rangeCheckForAdd(index); // Check subscript out of bounds
		ensureCapacity(size + 1); // Make sure the capacity is large enough
		
		// 0 1 2 3 4 5 6 7 8 9	(index)
		// 1 2 3 4 5 6 x x x x 	 (original array)
		// At index=2, insert 9 and move all elements back
		// 1 2 9 3 4 5 6 x x x 	 (array after add)
		// Start from back to front, move each element back one bit, and then assign a value
		for (int i = size - 1; i > index; i--) {
			elements[i + 1] = elements[i];
		}
		elements[index] = element; // copy
		size++;
	}
	/**
	 * Add element to last
	 */
	public void add(E element){
		add(size, element);
	}
	/**
	 * Element that sets the index location
	 * @param index
	 * @param element
	 * @return Original element ֵ
	 */
	public E get(int index){
		rangeCheck(index);
		return elements[index];
	}
	/**
	 * Element that sets the index location
	 * @param index
	 * @param element
	 * @return Original element ֵ
	 */
	public E set(int index, E element){
		rangeCheck(index);
		E old = elements[index];
		elements[index] = element;
		return old;
	}
	/**
	 * Delete element at index position
	 * @param index
	 * @return
	 */
	public E remove(int index){
		rangeCheck(index);
		// 0 1 2 3 4 5 	(index)
		// 1 2 3 4 5 6  	 (original array)
		// Delete the element with index 2 and move the element forward
		// 1 2 4 5 6 	 (array after remove)
		// Move from front to back, covering the front element with the rear element
		E old = elements[index];
		for (int i = index; i < size - 1; i++) {
			elements[i] = elements[i + 1];
		}
		elements[--size] = null; // After deleting the element, set the last bit to null
		return old;
	}
	/**
	 * View the index of the element
	 * @param element
	 * @return
	 */
	public int indexOf(E element){
		/*
		// It's OK not to handle null, but it's not robust enough
		for (int i = 0; i < size; i++) {
			if(elements[i].equals(element)) return i;
		}
		 */
		if(element == null){ // Process null
			for (int i = 0; i < size; i++) {
				if(elements[i] == null) return i;
			}
		}else{
			for (int i = 0; i < size; i++) {
				if(elements[i].equals(element)) return i;
			}
		}
		return ELEMENT_NOT_FOUND;
	}
	/**
	 * Clear all elements
	 */
	public void clear(){
		// Pay attention to memory management after using generic arrays (set elements to null)
		for (int i = 0; i < size; i++) {
			elements[i] = null;
		}
		size = 0;
	}
	/**
	 * Capacity expansion operation
	 */
	private void ensureCapacity(int capacity){
		int oldCapacity = elements.length;
		if(oldCapacity >= capacity) return;
		// The new capacity is 1.5 times the old capacity
		int newCapacity = oldCapacity + (oldCapacity >> 1);
		E[] newElements = (E[])new Object[newCapacity];
		for (int i = 0; i < size; i++) {
			newElements[i] = elements[i]; // Copy the original array elements to the new array
		}
		elements = newElements;
		System.out.println("size="+oldCapacity+", Expansion to"+newCapacity);
	}
	/****************Encapsulated function**************************/
	// Exception thrown when subscript is out of bounds
	private void outOfBounds(int index) {
		throw new IndexOutOfBoundsException("Index:" + index + ", Size:" + size);
	}
	// Check for subscript out of bounds (inaccessible or deleted location)
	private void rangeCheck(int index){
		if(index < 0 || index >= size){
			outOfBounds(index);
		}
	}
	// Check that the subscript of add() is out of bounds (you can add an element at the size position)
	private void rangeCheckForAdd(int index) {
		if (index < 0 || index > size) {
			outOfBounds(index);
		}
	}
	/****************Encapsulated function***************************/
	@Override
	public String toString() {
		// Print form: size=5, [99, 88, 77, 66, 55]
		StringBuilder string = new StringBuilder();
		string.append("size=").append(size).append(", [");
		for (int i = 0; i < size; i++) {
			if(0 != i) string.append(", ");
			string.append(elements[i]);
		}
		string.append("]");
		return string.toString();
	}
}

Test run:

public static void main(String[] args) {
	ArrayList<Person> list = new ArrayList<>();
	
	list.add(new Person(10, "jack"));
	list.add(new Person(20, "rose"));
	list.add(null);
	list.add(null);
	
	System.out.println("add()Add element: " + list);
	
	System.out.println("get()Get element: " + list.get(0));
	
	list.set(0, new Person(99, "ghost"));
	System.out.println("set()Set element value: " + list);
	
	list.remove(0);
	System.out.println("remove()Delete element: " + list);
	
	list.clear();
	System.out.println("clear()Empty array: " + list);
}

add()Add element: size=4, [Person [age=10, name=jack], Person [age=20, name=rose], null, null]
get()Get element: Person [age=10, name=jack]
set()Set element value: size=4, [Person [age=99, name=ghost], Person [age=20, name=rose], null, null]
remove()Delete element: size=3, [Person [age=20, name=rose], null, null]
clear()Empty array: size=0, []

Generic dynamic array source code (C + +)

#include<iostream>
using namespace std;
#define ELEMENT_NOT_FOUND -1;

template<typename E>
class Array
{
private:
	int m_size;		// Number of elements
	int m_capacity; // Array capacity
	E * m_elements;	// Point to first address
	void outOfBounds(int index) {
		throw index;
	}
	void rangeCheck(int index);	// Check subscript (get,set)
	void rangeCheckForAdd(int index); // Check subscript (add)
	void ensureCapacity(int capacity); // Check capacity and expansion
public:
	Array(int capacity = 10);
	~Array();
	int size(); // Number of elements
	bool isEmpty(); // Is it empty
	int indexOf(E element); // View the location of the element
	bool contains(E element); // Include an element
	E set(int index, E element); // Element that sets the index location
	E get(int index); // Returns the element corresponding to the index position
	void add(int index, E element); // Add an element to the index location
	void add(E element); // Add element to last
	E remove(int index); // Delete the element corresponding to the index position
	void clear(); // Clear all elements
};
template<typename E>
void Array<E>::rangeCheck(int index) {
	if (index < 0 || index >= m_size)
		outOfBounds(index);
}
template<typename E>
void Array<E>::rangeCheckForAdd(int index) {
	if(index < 0 || index > m_size)
		outOfBounds(index);
}
template<typename E>
void Array<E>::ensureCapacity(int capacity) {
	int oldCapacity = m_capacity;
	if (oldCapacity >= capacity) return;
	// The new capacity is 1.5 times the old capacity
	int newCapacity = oldCapacity + (oldCapacity >> 1);
	E *newElements = new E[newCapacity];
	for (int i = 0; i < m_size; i++) {
		newElements[i] = m_elements[i];
	}
	delete[] m_elements; // Release original space
	m_elements = newElements;
	m_capacity = newCapacity;
	cout << oldCapacity << "Capacity expansion is" << newCapacity << endl;
}

template<typename E>
Array<E>::Array(int capacity) {
	m_capacity = (capacity < 10) ? 10 : capacity;
	m_elements = new E[m_capacity];
}
template<typename E>
Array<E>::~Array() {
	delete[] m_elements;
}
template<typename E>
int Array<E>::size(){
	return m_size;
}
template<typename T>
bool Array<T>::isEmpty() {
	return m_size == 0;
}
template<typename E>
int Array<E>::indexOf(E element) {
	for (int i = 0; i < m_size; i++) {
		if (m_elements[i] == element) return i;
	}
	return ELEMENT_NOT_FOUND;
}
template<typename E>
bool Array<E>::contains(E element) {
	return indexOf(element) != ELEMENT_NOT_FOUND;
}
template<typename E>
E Array<E>::set(int index, E element) {
	rangeCheck(index);
	E old = element;
	m_elements[index] = element;
	return old;
}
template<typename E>
E Array<E>::get(int index) {
	rangeCheck(index);
	return m_elements[index];
}
template<typename E>
void Array<E>::add(int index, E element) {
	rangeCheckForAdd(index);
	ensureCapacity(m_size + 1);
	// 0 1 2 3 4 5 
	// 1 2 3 5 6 7
	// index=3, element=4
	for (int i = m_size; i > index; i--) {
		m_elements[i] = m_elements[i-1];
	}
	m_elements[index] = element;
	m_size++;
}
template<typename E>
void Array<E>::add(E element) {
	add(m_size, element);
}
template<typename E>
E Array<E>::remove(int index) {
	rangeCheck(index);
	E old = m_elements[index];
	// 0 1 2 3 4 5 
	// 1 2 3 5 6 7
	// index=2
	for (int i = index; i < m_size; i++) {
		m_elements[i] = m_elements[i + 1];
	}
	m_elements[--m_size] = NULL;
	return old;
}
template<typename E>
void Array<E>::clear() {
	// m_elements = nullptr; //  It is not feasible to directly clear the address pointed to by the whole pointer
	for (int i = 0; i < m_size; i++) {
		m_elements[i] = NULL;
	}
	m_size = 0;
}

int main() {
	Array<int> array;
	
	for (int i = 0; i < 30; i++) {
		array.add(i);
	}
	
	cout << "array.set(0, 99): " << array.set(0, 99) << endl;
	cout << "array.remove(0): " << array.remove(0) << endl;
	cout << "array.isEmpty(): " << array.isEmpty()<< endl;
	cout << "array.cotains(5): " << array.contains(5) << endl;
	cout << "size = " << array.size() << endl;
	array.add(10, 99); 
	cout << "array.add(10, 99), size = " << array.size() << endl;

	for (int i = 0; i < array.size() ;  i++) {
		if (i != 0) {
			cout << ", ";
		}
		cout << array.get(i);
	}
}

10 Capacity expansion is 15
15 Capacity expansion is 22
22 Capacity expansion is 33
array.set(0, 99): 99
array.remove(0): 99
array.isEmpty(): 0
array.cotains(5): 1
size = 29
array.add(10, 99), size = 30
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 99, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29

Topics: Java data structure array