数据结构与算法

数据结构

1. 数组(Array)

  • 定义:存储固定大小同类型元素的数据结构。

  • 关键源码

    int[] intArray = new int[10]; // 声明一个长度为10的整型数组,分配内存空间
    
  • 应用场景:存储固定数量同类型数据,如一周的天气温度。

2. ArrayList

  • 定义:基于动态数组实现的List接口实现类。

  • 关键源码

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确保数组容量足够,内部方法
        elementData[size++] = e; // 将元素添加到数组末尾,并更新size
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果是默认空数组
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 设置最小容量
        }
        ensureExplicitCapacity(minCapacity); // 显式确保容量
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++; // 修改计数器,用于迭代器的快速失败检测
    
        // 溢出保护代码
        if (minCapacity - elementData.length > 0)
            grow(minCapacity); // 扩容
    }
    
    private void grow(int minCapacity) {
        // 溢出保护代码
        int oldCapacity = elementData.length; // 旧容量
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity; // 如果新容量仍小于最小容量,则设置为最小容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity); // 处理非常大的容量情况
        // minCapacity通常接近size,所以这是个胜利的操作
        elementData = Arrays.copyOf(elementData, newCapacity); // 复制数组到新容量的数组
    }
    
  • 应用场景:存储数量不确定的数据,如动态的新闻列表。

3. LinkedList

  • 定义:基于双向链表实现的List接口实现类。

  • 关键源码

    public void add(E e) {
        linkLast(e); // 将元素添加到链表末尾
    }
    
    void linkLast(E e) {
        final Node<E> l = last; // 获取当前最后一个节点
        final Node<E> newNode = new Node<>(l, e, null); // 创建新节点,前驱为最后一个节点,后继为null
        last = newNode; // 更新最后一个节点为新节点
        if (l == null)
            first = newNode; // 如果原来链表为空,则新节点也是第一个节点
        else
            l.next = newNode; // 否则,将原来最后一个节点的后继指向新节点
        size++; // 更新链表大小
        modCount++; // 修改计数器
    }
    
  • 应用场景:频繁插入删除数据的场景,如音乐播放列表。

4. HashMap

  • 定义:基于哈希表实现的Map接口实现类。

  • 关键源码

    HashMap 是一个链表数组结构。它首先会创建一个长度为 16 的链表数组。插入键值对时,会通过键的哈希值与数组长度进行位与运算来计算插入数组的索引(Index)。如果索引位置的链表不为空,会循环遍历链表,检查是否存在相同的键。如果存在相同键,则更新该键对应的值;如果不存在,则创建一个新的节点并将其添加到链表的末尾。当链表长度超过 8 时,该链表会被转换为红黑树,以优化性能。

    此外,当哈希表中的元素数量超过一定阈值时,数组会进行扩容。这个阈值通常是数组长度乘以负载因子(默认负载因子为 0.75)。例如,如果数组长度为 16,负载因子为 0.75,那么当哈希表中的元素数量超过 12(16 × 0.75)时,数组会扩容。扩容时,会创建一个新的哈希表,其长度通常是原表的两倍,然后将原表中的所有键值对重新插入到新表中。

    1. 申请一个初始长度为16的数组

    HashMap中,数组通常用于存储桶(Bucket),每个桶可以是一个链表或红黑树的头节点。

    Node<K, V>[] table = new Node[DEFAULT_INITIAL_CAPACITY]; // 初始化数组
    

    2. 通过keyhash值计算index

    计算键的哈希值,并通过位与操作计算保存到数组的Index。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 扰动函数,提高哈希值的分布均匀性
    }
    
    int indexFor(int hash, int length) {
        return hash & (length - 1); // 计算索引, 位与保证inde不超过length-1
    }
    

    3. 将key-value插入到数组中

    key和value封装成Node,根据计算的索引,将Node插入到对应的桶中。如果桶不为空,遍历链表,检查是否存在相同的键,如果存在,更新该键对应的值;如果不存在,创建一个新的节点并添加到链表的末尾。如果链表长度超过8,链表会被转换成红黑树。

    public V put(K key, V value) {
        if (key == null) {
            throw new NullPointerException("Key cannot be null");
        }
    
        int hash = hash(key);
        int index = indexFor(hash, table.length);
    
        // 获取该索引位置的节点
        Node<K, V> node = table[index];
    
        // 检查是否已经存在相同的键
        if (node != null) {
            Node<K, V> current = node;
            while (current != null) {
                if (current.hash == hash && (current.key == key || key.equals(current.key))) {
                    // 键已存在,更新值
                    V oldValue = current.value;
                    current.value = value;
                    return oldValue;
                }
                current = current.next;
            }
    
            // 键不存在,插入新节点
            node = new Node<>(hash, key, value, node);
            table[index] = node;
            size++;
    
            // 检查链表长度,如果超过8,转换为红黑树
            if (node instanceof TreeNode) {
                ((TreeNode<K, V>) node).treeifyBin(table, index);
            } else if (size >= TREEIFY_THRESHOLD) {
                treeifyBin(table, index);
            }
        } else {
            // 桶为空,直接插入新节点
            table[index] = new Node<>(hash, key, value, null);
            size++;
        }
    
        // 检查是否需要扩容
        if (size > table.length * loadFactor) {
            resize();
        }
    
        return null;
    }
    

    4.数组长度的调整(扩容)

    当哈希表中的元素数量超过一定阈值时,数组会进行扩容。这个阈值通常是数组长度乘以负载因子(默认负载因子为0.75)。例如,如果数组长度为16,负载因子为0.75,那么当哈希表中的元素数量超过16 * 0.75 = 12时,数组会进行扩容。创建一个新的更大的哈希表(通常是原表长度的2倍),并将原表中的所有键值对重新插入到新表中。

    // 扩容操作
    private void resize() {
        int oldCapacity = table.length;
        int newCapacity = oldCapacity << 1; // 新容量为原容量的2倍
        Node<K, V>[] newTable = new Node[newCapacity];
    
        // 重新哈希并插入所有键值对
        for (int i = 0; i < oldCapacity; i++) {
            Node<K, V> node = table[i];
            if (node != null) {
                table[i] = null; // 清空旧桶
                if (node instanceof TreeNode) {
                    // 如果是红黑树,拆分为链表并重新哈希
                    ((TreeNode<K, V>) node).split(newTable, newCapacity, i);
                } else {
                    // 如果是链表,直接重新哈希
                    while (node != null) {
                        Node<K, V> next = node.next;
                        int newIndex = indexFor(node.hash, newCapacity);
                        node.next = newTable[newIndex];
                        newTable[newIndex] = node;
                        node = next;
                    }
                }
            }
        }
    
        table = newTable; // 更新引用
    }
    
  • 应用场景:根据键快速查找值,如存储用户ID与信息的映射。

5. SparseArray

  • 定义:基于整数键的优化数组结构。

  • 关键源码

    public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key); // 二分查找键的位置
    
        if (i >= 0) {
            mValues[i] = value; // 如果找到相同键,则更新值
        } else {
            i = ~i; // 计算插入位置
    
            if (i < mSize && mValues[i] == DELETED) { // 如果当前位置是已删除的节点
                mKeys[i] = key; // 更新键
                mValues[i] = value; // 更新值
                return;
            }
    
            if (mGarbage && mSize >= mKeys.length) { // 如果有垃圾节点且需要扩容
                gc(); // 回收垃圾节点
    
                // 因为索引可能变化,所以重新查找
                i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    
                if (i >= 0) {
                    mValues[i] = value; // 更新值
                    return;
                }
    
                i = ~i; // 计算插入位置
            }
    
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); // 插入键
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); // 插入值
            mSize++; // 更新大小
        }
    }
    
  • 应用场景:存储视图ID与对象的映射。

6. LruCache

  • 定义:基于LRU算法的缓存类。

  • 关键源码

    public final void put(Key key, Value value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }
    
        synchronized (this) {
            cache.put(key, value); // 将键值对放入缓存
            afterPut(key, value); // 插入后的处理
        }
    }
    
    private void afterPut(K key, V value) {
        int cost = sizeOf(key, value); // 计算新元素的大小,通常为元素的字节数
        if (cost < 0) {
            throw new IllegalStateException("Negative size: " + key + "=" + value);
        }
    
        pruneToSize(cost); // 根据新元素大小,修剪缓存以满足最大容量限制
    }
    
    private void pruneToSize(int size) {
        while (true) {
            if (size <= maxWeight) { // 如果当前大小不超过最大容量,则退出
                break;
            }
    
            Map.Entry<K, V> toEvict = cache.eldest(); // 获取最老的元素(最近最少使用的元素)
            if (toEvict == null) { // 如果没有元素可移除,则退出
                break;
            }
    
            K key = toEvict.getKey(); // 获取最老元素的键
            V value = toEvict.getValue(); // 获取最老元素的值
            cache.remove(key); // 从缓存中移除最老元素
            size -= sizeOf(key, value); // 更新当前大小
            entryRemoved(true, key, value, null); // 调用回调方法,通知元素被移除
        }
    }
    
  • 应用场景:图片缓存等,通过缓存最近使用的数据,提高数据访问效率。

算法

排序算法

  • 冒泡排序

    冒泡排序是一种简单的排序算法,通过重复遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行直到没有再需要交换,也就是说该数列已经排序完成。

    public static void bubbleSort(int[] arr) {
        int n = arr.length;
        boolean swapped;
        for (int i = 0; i < n - 1; i++) {
            swapped = false; // 用于优化,如果某一轮没有发生交换,说明数组已经排序完成
            for (int j = 0; j < n - 1 - i; j++) {
                // 比较相邻元素,如果前一个元素大于后一个元素,则交换
                if (arr[j] > arr[j + 1]) {
                    // 交换 arr[j] 和 arr[j + 1]
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    swapped = true; // 发生了交换
                }
            }
            // 如果某一轮没有发生交换,说明数组已经排序完成
            if (!swapped) {
                break;
            }
        }
    }
    
  • 快速排序(Quick Sort)

    快速排序是一种高效的排序算法,采用分治法的思想。它选择一个元素作为“基准”(pivot),将数组分为两部分,一部分包含比基准小的元素,另一部分包含比基准大的元素,然后递归地对这两部分进行快速排序。

    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // pi 是分区索引,arr[pi] 现在位于正确位置
            int pi = partition(arr, low, high);
    
            // 递归排序分区的两部分
            quickSort(arr, low, pi - 1); // 排序左半部分
            quickSort(arr, pi + 1, high); // 排序右半部分
        }
    }
    
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high]; // 选择最后一个元素作为基准
        int i = (low - 1); // 较小元素的索引
    
        for (int j = low; j < high; j++) {
            // 如果当前元素小于或等于基准
            if (arr[j] <= pivot) {
                i++;
    
                // 交换 arr[i] 和 arr[j]
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    
        // 交换 arr[i+1] 和 arr[high] (或基准)
        int temp = arr[i + 1];
        arr[i + 1] = arr[high];
        arr[high] = temp;
    
        return i + 1; // 返回分区索引
    }
    

搜索算法

  • 二分查找

    二分查找是一种在有序数组中查找特定元素的高效算法。它通过将数组分为两半,比较中间元素与目标值,根据比较结果决定在左半部分还是右半部分继续查找,直到找到目标值或搜索范围为空。

    public static int binarySearch(int[] arr, int target) {
        int low = 0;
        int high = arr.length - 1;
    
        while (low <= high) {
            int mid = low + (high - low) / 2; // 防止 (low + high) 溢出
    
            // 检查中间元素是否是目标值
            if (arr[mid] == target) {
                return mid; // 找到目标值,返回索引
            }
    
            // 如果目标值大于中间元素,搜索右半部分
            if (target > arr[mid]) {
                low = mid + 1;
            } else {
                // 如果目标值小于中间元素,搜索左半部分
                high = mid - 1;
            }
        }
    
        return -1; // 目标值不存在,返回 -1
    }
    
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值