数据结构与算法实战(三)基础数据结构
一、数组
数组最大优点:快速查询
1、二次封装属于我们的数组类
public class Array {
private int[] data;
private int size;
//构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data = new int[capacity];
size = 0;
}
//无参构造函数,传入数组的容量capacity = 10
public Array(){
this(10);
}
//获取数组中的元素个数
public int getSize(){
return size;
}
//获取数组的容量
public int getCapacity(){
return data.length;
}
//返回数组是否为空
public boolean isEmpty(){
return size == 0;
}
}
2、向数组中添加元素
向数组末添加元素
public void addLast(int e){
if(size == data.length)
throw new IllegalArgumentException("AddLast failed,Array is already full");
data[size] = e;
size++;
}
向指定位置添加元素
//向指定位置添加元素
public void add(int index,int e){
if(size == data.length)
throw new IllegalArgumentException("AddLast failed,Array is already full");
if(index < 0 || index > size)
throw new IllegalArgumentException("AddLast failed,Required index >= 0 and index <= size");
for (int i = size - 1; i >= index; i--) {
data[i + 1] = data[i];
}
data[index] = e;
size++;
}
向数组头添加元素
//向数组头添加元素
public void addFirst(int e) {
add(0, e);
}
重写toString方法
@Override
public String toString() {
StringBuilder res= new StringBuilder().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();
}
3、查询和修改元素
//获取index索引位置的元素
int get(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Get failed.Index is illegal");
return data[index];
}
//修改index索引位置的元素为e
void set(int index, int e){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Get failed.Index is illegal");
data[index] = e;
}
4、包含、搜索和删除元素
//查找数组在是否含有元素e
public boolean contains(E e){
for (int i = 0; i < size; i++) {
if(data[i].equals(e) )
return true;
}
return false;
}
//查找数组中元素e所在的索引,如果不存在元素e,则返回-1
public int find(E e){
for (int i = 0; i < size; i++) {
if (data[i].equals(e))
return i;
}
return -1;
}
//从数组中删除index位置的元素,返回删除元素
public E remove(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed.Index is illegal");
E ret = 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 ret;
}
// 从数组中删除第一个元素,返回删除元素
public E removeFirst(){
return remove(0);
}
//从数组中删除最后一个元素,返回删除元素
public E removeLast(){
return remove(size-1);
}
//从数组中删除元素e
public void removeElement(E e){
int index = find(e);
if(index != -1)
remove(index);
}
//动态数组
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
5、动态添加元素
//向指定位置添加元素
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("AddLast failed,Required index >= 0 and index <= size");
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++;
}
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
}
6、分析动态数组的时间复杂度
添加操作 O(n)
addLast(e) : O(1)
addFirst(e) : O(n)
add(index , e) : O(n)
删除操作 O(n)
removeLast(e) : O(1)
removeFirst(e) : O(n)
remove(index , e) : O(n)
修改操作(支持随机访问) O(1)
set(index , e)
查询操作
get(index) O(1)
contains(e) O(n)
find(e) O(n)
二、栈
- 栈是一种线性结构
- 相比数组,栈对应的操作是数组的子集
- 只能从栈顶添加或取出元素
- 后进先出(LIFO)
栈的实现
先定义一个接口
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E push();
}
基于之前说过的动态数组实现栈结构
//基于动态数组,实现栈
public class ArrayStack<E> implements Stack {
Array<E> array;
public ArrayStack(int capacity){
array = new Array<>(capacity);
}
public ArrayStack(){
array = new Array<>();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public void push(Object o) {
array.addLast((E) o);
}
@Override
public E pop() {
return array.removeLast();
}
@Override
public E peek() {
return array.getLast();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Stack :");
res.append("[");
for (int i = 0; i < array.getSize(); i++) {
res.append(array.get(i));
if(i != array.getSize() -1)
res.append(", ");
}
res.append("] top");
return res.toString();
}
}
注意:栈的五个基本操作都是O(1)的时间复杂度
栈的应用
20. 有效的括号
难度简单1947
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(c == '(' || c == '{' || c == '[')
stack.push(c);
else{
if(stack.isEmpty())
return false;
char topChar = stack.pop();
if(c == ')' && topChar != '(')
return false;
if(c == ']' && topChar != '[')
return false;
if(c == '}' && topChar != '{')
return false;
}
}
return stack.isEmpty();
}
三、队列
- 也是一种线性结构
- 相比数组,队列的操作是数组的子集
- 只能从队尾添加元素,只能从队首取出元素
- 先进先出
数组队列的实现
先定义一个接口
public interface Queue<E> {
int getSize();
boolean isEmpty();
void enqueue();
E dequeue();
E getFront();
}
public class ArrayQueue<E> implements Queue {
private Array<E> array;
public ArrayQueue(int capacity){
array = new Array<>(capacity);
}
public ArrayQueue(){
array = new Array<>();
}
@Override
public int getSize() {
return array.getSize();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
@Override
public void enqueue(Object e) {
array.addLast((E) e);
}
@Override
public E dequeue() {
return array.removeFirst();
}
@Override
public E getFront() {
return array.getFirst();
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("Queue :");
res.append("front [");
for (int i = 0; i < array.getSize(); i++) {
res.append(array.get(i));
if(i != array.getSize() -1)
res.append(", ");
}
res.append("] tail");
return res.toString();
}
ArrayQueue的复杂度分析
除了dequeue() 为O(n) ,其余都为O(1)
数组队列的问题
删除首元素是O(n)的复杂度
由此引出循环队列
循环队列
front指向首元素,tail指向尾元素的后一个位置
起始的时候front == size 队列为空,然后一直向后添加元素a,b…,直到添加到f的时候满了,现在删除a,b两元素,front指向c,然后计算(tail+1) % capacity ,这是tail新指向的索引
当(tail+1) % capacity == front 时,队列满
capacity中会浪费一个空间
代码实现
public class LoopQueue<E> implements Queue{
private E[] data;
private int front , tail;
private int size;
public LoopQueue(int capacity){
data = (E[]) new Object[capacity+1];
front = 0;
tail = 0;
size = 0;
}
public LoopQueue(){
this(10);
}
public int getCapacity(){
return data.length - 1; //数组中会有一个位置被有意识的浪费掉
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return tail == front;
}
@Override
public void enqueue(Object e) {
if((tail+1) % data.length == front)
resize(getCapacity() * 2);
data[tail] = (E)e ;
tail = (tail + 1) % data.length;
size ++;
}
private void resize(int newCapacity) {
E[] newData = (E[]) new Object[newCapacity + 1];
for (int i = 0; i < size; i++) {
newData[i] = data[(i + front) % data.length];
}
data = newData;
front = 0;
tail = size;
}
@Override
public E dequeue() {
if(isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
E ret = data[front];
data[front] = null;
front = (front + 1) % data.length;
size --;
if(size == getCapacity() / 4 && getCapacity()/2 != 0)
resize(getCapacity()/2);
return ret;
}
@Override
public E getFront() {
if(isEmpty())
throw new IllegalArgumentException("Queue is empty");
return data[front];
}
@Override
public String toString() {
StringBuilder res= new StringBuilder().append(String.format("Queue : size = %d, capacity = %d \n",size,getCapacity()));
res.append("front [");
for (int i = front; i != tail; i = (i+1) % data.length) {
res.append(data[i]);
if((i+1) % data.length != tail )
res.append(", ");
}
res.append("] tail");
return res.toString();
}
public static void main(String[] args) {
LoopQueue<Integer> queue = new LoopQueue<>();
for (int i = 0; i < 10; i++) {
queue.enqueue(i);
System.out.println(queue);
if(i % 3 == 2){
queue.dequeue();
System.out.println("1111"+queue); }
}
}
}
数组队列和循环队列性能比较
import java.util.Random;
public class Main {
public static void main(String[] args) {
int opCount = 200000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
double time1 = testQueue(arrayQueue,opCount);
System.out.println("ArrayQueue , time: "+ time1 + "s");
LoopQueue<Integer> loopQueue = new LoopQueue<>();
double time2 = testQueue(loopQueue,opCount);
System.out.println("LoopQueue , time: "+ time2 + "s");
}
//测试使用队列q运行opCount个入队、出队操作所需要的时间,单位:秒
private static double testQueue(Queue<Integer> q, int opCount){
long startTime = System.nanoTime();
Random random = new Random();
for (int i = 0; i < opCount; i++) {
q.enqueue(random.nextInt(Integer.MAX_VALUE));
}
for (int i = 0; i < opCount; i++) {
q.dequeue();
}
long endTime = System.nanoTime();
return (endTime - startTime)/1000000000.0;
}
}
155. 最小栈
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
push(x)
—— 将元素 x 推入栈中。pop()
—— 删除栈顶的元素。top()
—— 获取栈顶元素。getMin()
—— 检索栈中的最小元素。
import java.util.Stack;
/**
* 解题思路:
* 使用一个栈,每输入一个数字,入栈两次,第一次是自身元素,第二次是入栈元素和栈顶元素之间的最小值
* 保持栈顶就是整个栈中元素的最小值
* 例如输入2,3,1。 在栈中就是 2,2,3,2,1,1
*/
public class MinStack {
private Stack<Integer> stack;
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<>();
}
public void push(int x) {
if(stack.isEmpty()){
stack.push(x);
stack.push(x);
}else {
int temp = stack.peek();
stack.push(x);
if(x > temp){
stack.push(temp);
}else {
stack.push(x);
}
}
}
public void pop() {
stack.pop();
stack.pop();
}
//获取栈顶元素
public int top() {
return stack.get(stack.size() - 2);
}
public int getMin() {
return stack.peek();
}
}