数据结构与算法第二篇

数组和链表

 

No.1 线性表

线性表是将数据按照一定规则排列起来的数据,有前继,有后继。一般的,可以将所有的数据结构分为线性结构和非线性结构,线性结构中有,数组,链表,栈,对列。

线性结构定义:n个数据元素的有序(次序)集合,其特征为:

1.集合中必须存在唯一的一个“第一个元素”。

2.集合中必须存在唯一的一个“最后一个元素”。

3.除了最后元素之外,其他元素必须有唯一的“后继”。

4.除了第一个元素外,其他元素必须有唯一的“前驱”。

 

No.2数组基础

数组是一种用连续的内存空间存储相同类型的线性数据结构的工具。

相同数据类型的数据,每个数据占据内存空间的大小也是固定的,所以整个数组占据内存的空间也是固定的,根据数据量的大小来决定。

No.3 数组开辟空间的过程

数组产生地址空间是在栈内存中,开辟内存空间是在堆内存中。数组很特别,例如存入int类型的数据的时候,int类型占4个字节,在开辟了地址空间后,每个数据依次存入数组后都是以4个字节的地址增加存入数据,首地址在初始化定义数组的时候,就会被分配内存地址。

数组中有一个特别的寻址公式,假设首元素的地址为baseAddress 为array[0],要查找数组中3号元素的内存地址,使用公式baseAddress+i*dataSize,其中i变量是要查找的第几号元素,dataSize是数据的大小,占据多少个字节。

No.4 数组的特点

1.数组查询的时间复杂度为O(1)

怎么理解?因为数组的长度是固定的,不会随着要查询的下标而影响整个代码的执行时间,只需要通过寻址公式就能找到要查询的元素,所以很简单,时间复杂度就是O(1)

2.数组的查询和删除操作的复杂度是O(n)

类比,当一个数组中需要插入一个元素,首先得考虑到元素插入的位置,在尾部插入,时间复杂度肯定是O(1),在数组中间插入的话,看具体插入的位置,中间插入的复杂度是O(n),通过计算,算出平均时间复杂度也是O(n).

3.提高数组删除效率的方法,打标记方法(Jvm中垃圾回收中核心的算法)

在一个数组中为了提高删除的效率,可以将要删除的数据打上标记,在某些特定的条件下,我们追求不一定是数组中数据的连续性,如果我们使用多次的删除操作放在一起的话,是不是删除的效率就很提高很多?

打标记删除操作就是为了提高数组的删除效率,在一个数组中有以下几个元素,a1,a2,a3,a4,a5,a6. 若要删除的元素是a1,a2,a5,可以将 a1,a2,a5打上标记,然后删除标记后统一将数据往前挪,这样可以一次性将数据删除,增强了数组的删除效率。

 

No.5 实战面试题(leetcode11:盛最多水的容器)

1.暴力解法,列举出所有面积存在的可能性,进行暴力求解。

public class Solution{
	public int maxArea(int [] arr){
        // 定义最大面积max
        int max = 0;
        // 双层循环求出最大面积
        for(int i = 0;i<arr.length;i++){
            for(int j = i+1;j<arr.length;j++){
                // 定义出最大面积的求法
                int area = (j-i)*Math.min(arr[i],arr[j]);
                // 更新面积
                max=area>max?area:max;
            }
        }
        return max;
    }
}

注:这种解法就真的是暴力求解的方法,用第一个i下标的值(长度)去乘上下一个j下标对应的值,这样可以算出每一个围成的图形面积,再更新面积后可以求出最大面积。(为什么是用最短的那个作为长度来乘宽度?因为类比木桶装水的原理是取决于最短的那根,而不是最长的那根。)

2.双指针思想(左右指针,夹逼思想)

public class Solution(){
		public int maxArea(int[] arr){
            // 定义最大面积max
            int max = 0;
			// 定义双指针
            int i = 0; // 从数组中的第一个元素开始
            int j = arr.length-1;// 从数组中最后一个元素开始
            // 判断当i往前走,j往回走,两个不会相遇
            while(i!=j){
                int area = (j-i)*Math.min(arr[i],arr[j]);
                if(arr[i]<arr[j]){
                    i++;
                }else{
                    j--;
                }
               max=area>max?area:max;
            }
            
            return max;
		}
}

理解:一开始两个指针一个指向开头一个指向结尾,此时容器的底是最大的,接下来随着指针向内移动,会造成容器的底变小,在这种情况下想要让容器盛水变多,就只有在容器的高上下功夫。 那我们该如何决策哪个指针移动呢?我们能够发现不管是左指针向右移动一位,还是右指针向左移动一位,容器的底都是一样的,都比原来减少了 1。这种情况下我们想要让指针移动后的容器面积增大,就要使移动后的容器的高尽量大,所以我们选择指针所指的高较小的那个指针进行移动,这样我们就保留了容器较高的那条边,放弃了较小的那条边,以获得有更高的边的机会。

 

No.6 双指针思想之快慢指针(leetCode 283 移动零)

给一个指定数组,例如 int arr [] = {1,2,0,0,3,5},设计代码将数组中的元素重新排序,并且非零元素在前,0排在最后。

public class Solution{
	public void moveZeros(int[] arr){
        // 先判断数组是否为空
        if(arr == null || arr.length<2){
            return;
        }
        // 在不为空的情况下
        int i = 0;
        int j = 0;
        for(;i<arr.length;i++){
            if(arr[i]!= 0){
                arr[j]=arr[i];
                j++;
            }
        }
        // 当依次排序后,剩下末尾的j的值为0.并且要对j坐标的元素赋值
        while(j<arr.length){
            arr[j]=0;
            j++;
        }
    }
}

仔细思考后发现,j的值可不可以在一次循环内全部进行完,也就是i经过一次循环之后,j坐标上的值已经是非0元素了,i是快指针,j是慢指针,所以i在跳出循环之后,j的坐标依然是小于i的,所以可以考虑改进此代码。

public class Solution{
    public void moveZeros(int[] arr){
        // 判断数组是否为空
        if(arr == null || arr.length<2){
            return;
        }
        // 不为空时
        int i = 0;
        int j = 0;
        int temp = 0;
        for(;i++;i<arr.length){
            if(arr[i]!=0){
                temp = arr[j];
                arr[j]=arr[i];
                arr[i]=temp;
                j++;
            }
        }
    }
}

这样下来一次循环就解决了整个问题!

No.6 动态数组

动态数组的提出:

1.java中底层用数组来存储数据,但是存储机制是不能进行容量扩容的,而且插入数据和删除数据都很麻烦,因此容器要屏蔽底层的数组操作细节,支持数据的查询,插入,删除等操作。

2.java的数组一旦被申请了之后是不能改变容量的大小的,提出有容器的支持,来支持扩容机制。

下面就是手写ArrayList作为容器,底层原理是基于数组的扩容。

List接口:

package com.itcast.List;

public interface List {


    /**
     * 返回容器中元素的个数
     * @return 返回int类型的元素个数
     */
    int size();


    /**
     * 判断容器是否为空
     * @return 返回布尔类型判断
     */
    boolean isEmpty();


    /**
     * 传入一个数据判断数据在数组中索引的位置
     * @param o 传入的参数o
     * @return 返回o在数组中的位置
      */
    int indexOf(int o);


    /**
     * 判断是否包含传入的元素
     * @param o 传入的参数o
     * @return 返回布尔类型的数据
     */
     boolean contains(int o);


    /**
     * 将数据添加到容器末尾,并判断是否添加成功。
     * @param o 传入的参数o
     * @return 返回布尔类型数据
     */
       boolean add(int o);


    /**
     * 在指定位置添加元素
     * @param index  被指定的索引位置
     * @param element 添加的元素element
     */
    void add(int index,int element);


    /**
     * 在指定位置设置元素的值
     * @param index  指定的索引位置
     * @param element 修改的参数
     * @return  返回被设置后的元素
     */
    int set(int index,int element);

    /**
     * 删除元素
     * @param o 参数
     * @return 返回被删除的元素
     */
    int remove(int o);

    /**
     * 获取容器中的元素
     * @param o 参数
     * @return 返回获取到的元素
     */
    int  get(int o);
    /**
     * 清空容器
     */
    void clear();


}

发现实现一个简单的底层代码真的不同意哈,必须得先搞清楚底层原理,那么来了,下次面试问到了ArrayList怎么实现的?我肯定会啦!一定要会自己手写,不看代码的情况下写出来!

ArrayLis实现代码:

package com.itcast.List;

public class ArrayList implements List {

    // 定义一个数组
    private int [] elementDate;

    // 定义数组的大小
    private int size;

    // 定义默认长度
    private  final int DEFAULT_CAPATICY=10;

    // 使用构造方法初始化数组
    public ArrayList(){
       elementDate = new int[this.DEFAULT_CAPATICY];
    }
    // 使用构造方法创建指定大小数组
    public ArrayList(int capaticy){
        if(capaticy>0){
            elementDate= new int[capaticy];
        }else {
            throw new IllegalArgumentException("capaticy is error"+capaticy);
        }
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public int indexOf(int o) {
        for(int i = 0;i<size;i++){
            if (elementDate[i]==o){
                return i;
            }
        }
        return -1;
    }

    @Override
    public boolean contains(int element) {
        // 复用indexOf方法,如果返回-1则没有,返回其他数据表示有。
        return indexOf(element)>-1;
    }

    @Override
    public boolean add(int element) {
        // 在添加之前确定数组是否容量够,不够则要进行扩容。
        ensureCapacity(size+1);
        // 添加元素
        this.elementDate[size]=element;
        size++;
        return true;
    }

    @Override
    public void add(int index, int element) {
        // 索引检查
        indexCheck(index);
        // 检查容量
        ensureCapacity(size+1);
        // 添加数据
        System.arraycopy(this.elementDate,index,this.elementDate,index+1,size-index);
        this.elementDate[index]=element;
        // size+1
        size++;
    }

    @Override
    public int set(int index, int element) {
        // 索引检查
        indexCheck(index);
        // 保存没有被设置之前的数据
        int oldValue= this.elementDate[index];
        // 指定位置添加
        this.elementDate[index]=element;
        return oldValue;
    }

    @Override
    public int remove(int index) {
        // 索引检查
        indexCheck(index);
        // 保存移除之前指定索引位置的元素
        int oldValue = this.elementDate[index];
        // 移除操作
        System.arraycopy(this.elementDate,index+1,this.elementDate,index,size-index-1);
        // 总体前移一位,覆盖o位置上的元素,但是最后一个索引位置上的元素仍然还在,必须做清理
        this.elementDate[size-1]=0;
        size--;
        return oldValue;
    }

    @Override
    public int get(int index) {
        // 索引检查
        indexCheck(index);
        // 获取数据
        return this.elementDate[index];
    }

    @Override
    public void clear() {
        // 遍历数组,每个元素赋值为0,并且清空数组的长度
        for (int i = 0;i<size;i++){
            this.elementDate[i]=0;
        }
        // 清空数组长度
        size=0;

    }

    @Override
    public String showList() {
        StringBuilder st = new StringBuilder();
        if(size==0){
            st.append("list为空!");
        }else {
            st.append("list=[");
            // 遍历elementData
            for (int i = 0;i<size;i++){
                if(i==size-1){
                    st.append(this.elementDate[i]+"]");
                }else {
                    st.append(this.elementDate[i]+",");
                }
            }

        }
      //  st.append("]");
        return st.toString();

    }


    private void ensureCapacity(int minCapaticy) {
        if(minCapaticy>elementDate.length){
            grow(minCapaticy);
        }
    }

    private void grow(int minCapaticy) {
        int oldCapacity = this.elementDate.length;
        int newCapacity = oldCapacity + oldCapacity >> 1;
        if (newCapacity<minCapaticy){
            newCapacity = minCapaticy;
        }
        // 实现扩容
        copyOf(newCapacity);
    }

    private void copyOf(int newCapacity) {
        int[] newArray = new int [newCapacity];
        // 复制数据
        System.arraycopy(this.elementDate,0,newArray,0,size);
        // 将旧的数组废弃,指向新的数组,同时jvm会回收旧的数组占用的内存。
        this.elementDate = newArray;

    }

    private void indexCheck(int index) {
        if(index < 0 || index > size){
            throw new IndexOutOfBoundsException("index bound "+"index="+index+"size="+size);
        }
    }
}

测试代码(手写的,实现了所有的功能!)

package com.itcast.TestArrayList;

import com.itcast.List.ArrayList;

public class ArrayListTest {
    public static void main(String[] args) {
        // 初始化数组长度
        ArrayList list = new ArrayList(10);
        // 查看数组长度
        System.out.println(list.size());

        // 添加元素
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);

        // 查看数组长度
        System.out.println(list.size());

        // 继续添加元素
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);
        list.add(10);
        list.add(11);
        // 查看数组长度和数组
        System.out.println(list.showList());
        System.out.println(list.size());

        // 删除9号索引位置的元素
        list.remove(9);
        System.out.println(list.showList());
        System.out.println(list.size());

        // 指定8号索引位置添加元素为23
        list.add(8,23);
        System.out.println(list.showList());
        System.out.println(list.size());

        // 将8号索引位置的元素的值设为24
        list.set(8,24);
        System.out.println(list.showList());


        // 获取到8号索引位置的值
        System.out.println(list.get(8));

        // 移除8号索引位置的元素,并且查看数组长度和遍历数组
        list.remove(8);
        System.out.println(list.showList());
        System.out.println(list.size());

        // 清空数组,查看数组和数组长度
        list.clear();
        System.out.println(list.size());
        System.out.println(list.showList());
    }


}

No.7 实现动态数组的小总结

1.扩容!怎么实现扩容的一个机制,面试的时候肯定说啊就是默认容量满了之后,再扩大到之前的1.5倍,但是怎么进行扩容的呢?面试官让你手写扩容代码怎么办?所以得搞懂原理,当需要添加一个元素进去的时候,捏一个sureCapacity(size+1)的方法,实现sureCapacity的方法,首先要判断minCapacity>element.size?,则实现扩容机制,定义旧的容量为oldCapacity=element.length,在旧的capacity上增加:int newCapacity = oldCapacity+oldCapacity>>1;如果newCapacity<minCapacity,则将newCapacity=minCapacity,再实现数据的转移操作,创建capyOf(newCapacity)的方法,在方法中,定义一个新的数组,int[] newArray = new int [capacity],所有的步骤大概就是这样的一个思路。

 

2.检查索引是否合理的方法,在添加,删除中,必须得判断当前索引位置是否合理,定义checkIndex(index)的方法,如果index<0 || index>size,都是不合理的!

 

3.ArrayList在开发中用的很多,是因为不考虑存储的数据是否为基本类型,那么对于数组,只能存储基本的数据类型,不能存储引用数据类型,所以,就像老师总结的,进行底层的网络框架开发可能使用到数组的例子多。

 

总结一下学习算法的经验:一道题起码要刷十遍,而且学算法每天都要抽时间来学习,敲!理解后一个个敲出来! 没有坚持学是不行的哦!

有不对的地方还希望大家共同指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值