数据结构基础

本文详细探讨了数据结构中的数组和链表底层存储、扩容与操作技巧,包括合并有序链表的方法,以及如何使用优先级队列(如最大堆和最小堆)进行操作,如插入、删除和排序。文中还强调了虚拟节点在处理边界情况中的作用。
摘要由CSDN通过智能技术生成

一、数据结构的底层存储

数组:

  • 紧凑连续存储,可以随机访问
  • 可以通过索引快速找到对应元素,相对节约存储空间
  • 由于连续存储,内存空间必须一次性分配
    • 若要扩容,需要重新分配一块更大的空间,再把数据全部复制过去
    • 若要进行插入或删除,每次必须搬移后面的所有数据以保持连续

链表:

  • 不连续存储,靠指针指向下一个元素的位置————不存在数组扩容问题
  • 若知道某一元素的前驱和后继,操作指针即可删除该元素或插入新元素
  • 由于存储空间不连续,不能根据一个索引算出对应元素的地址,即不能随机访问
  • 由于每个元素必须存储前后元素位置的指针,会消耗更多的存储空间

二、数据结构的基本操作

基本操作:遍历或访问——> 增删改查
遍历/访问的基本形式:线性——for/while迭代和非线性——迭代

数组遍历框架:

void traverse(int[] arr){
	for(int i = 0; i < arr.length;i++){
		//访问arr[i]
	}
}

链表遍历框架:

//基本的单链表结构
clas ListNode{
	int val;
	ListNode next;
}

void traverse(ListNode head){
	for(ListNode p = head;p != null;p = p.next){
		//访问 p.val
	}
}

void traverse(ListNode head){
	traverse(head.next)
}

二叉树遍历框架:

//基本的二叉树节点
class TreeNode{
	int val;
	TreeNode left,right;
}

void traverse(TreeNode root){
	traverse(root.left);
	traverse(root.right);
}

N叉数遍历框架:

class TreeNode{
	int val;
	TreeNode[] children;
}

void traverse(TreeNode root){
	for(TreeNode child:root.children)
		traverse(child)
}

三、单链表操作

1. 合并两个有序列表

我的思路:

class ListNode{
	int val;
	ListNode next;
}

ListNode mergeTwoLists(ListNode l1, ListNode l2){
	ListNode res = null; //这里写错了
	while(l1.next != null || l2.next != null){// 条件判别有问题
		if(l1.val < l2.val){
			res.next = l1;
			l1 = l1.next;
		}
		else if(l1.val > l2.val){
			res.next = l2;
			l2 = l2.next;
		}else{
			res.next=l1;
			l1 = l1.next;
			l2 = l2.next;
		}
	}
	if(l1 != null){
		res.next = l1;
	}
	if(l2 != null){
		res.next = l2;
	}
	return res.next;
	
}

参考代码思路:

ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    // 虚拟头结点
    ListNode dummy = new ListNode(-1), p = dummy;
    ListNode p1 = l1, p2 = l2;
    
    while (p1 != null && p2 != null) {
        // 比较 p1 和 p2 两个指针
        // 将值较小的的节点接到 p 指针
        if (p1.val > p2.val) {
            p.next = p2;
            p2 = p2.next;
        } else { //剩余的条件是<=,等于是可以包含进来的
            p.next = p1;
            p1 = p1.next;
        }
        // p 指针不断前进
        p = p.next; // 一开始p指向虚拟节点
    }
    
    if (p1 != null) {
        p.next = p1;
    }
    
    if (p2 != null) {
        p.next = p2;
    }
    
    return dummy.next;
}

总结

  • 不要对链表直接操作,应该有一个索引指针,用该指针来
  • 可以设置虚拟节点,但不是设为null,返回时只要返回虚拟节点的next就行了
  • 注意 java 是 new 一个出来
  • 注意代码的简洁性

当你需要创建一条新链表的时候,可以使用虚拟节点简化边界情况的处理(dummy节点)

2. 链表的分解

我的思路:

public ListNode partition(ListNode head, int x) {
        ListNode left = new ListNode(-1);
        ListNode right = new ListNode(-1);
        ListNode p1 = left;
        ListNode p2 = right;
        ListNode q = head;
        while(q != null){
            if(q.val < x){
                p1.next = q;
                p1 = p1.next;
            }else{
                p2.next =q;
                p2 = p2.next;
            }
           q = q.next; //错误
        }
        p1.next = right.next;
        return left.next;
    }

参考代码:

ListNode partition(ListNode head, int x) {
    // 存放小于 x 的链表的虚拟头结点
    ListNode dummy1 = new ListNode(-1);
    // 存放大于等于 x 的链表的虚拟头结点
    ListNode dummy2 = new ListNode(-1);
    // p1, p2 指针负责生成结果链表
    ListNode p1 = dummy1, p2 = dummy2;
    // p 负责遍历原链表,类似合并两个有序链表的逻辑
    // 这里是将一个链表分解成两个链表
    ListNode p = head;
    while (p != null) {
        if (p.val >= x) {
            p2.next = p;
            p2 = p2.next;
        } else {
            p1.next = p;
            p1 = p1.next;
        }
        // 断开原链表中的每个节点的 next 指针
        ListNode temp = p.next;
        p.next = null;
        p = temp;
    }
    // 连接两个链表
    p1.next = dummy2.next;

    return dummy1.next;
}

总结:

  • 比较完之后,有一个将原链表与已找到的元素断开的一步,否则,会让P1和P2连接在一块【画图就知道了】

3.合并 k 个有序链表

提示: 使用优先级队列(二叉堆)

代码参考:

ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0) return null;
    // 虚拟头结点
    ListNode dummy = new ListNode(-1);
    ListNode p = dummy;
    // 优先级队列,最小堆
    PriorityQueue<ListNode> pq = new PriorityQueue<>(
        lists.length, (a, b)->(a.val - b.val));
    // 将 k 个链表的头结点加入最小堆
    for (ListNode head : lists) {
        if (head != null)
            pq.add(head);
    }

    while (!pq.isEmpty()) {
        // 获取最小节点,接到结果链表中
        ListNode node = pq.poll();
        p.next = node;
        if (node.next != null) {
            pq.add(node.next);
        }
        // p 指针不断前进
        p = p.next;
    }
    return dummy.next;
}


3.1 二叉堆 Binary Heap

  • 主要操作:sink(下沉)swim(上浮)
  • 主要应用:
    • 一种排序方法——堆排序
    • 一种有用的数据结构——优先级队列
  • 主要思想:逻辑上是一种特殊的二叉树(完全二叉树),但存在数组里——可以将数组索引作为指针
// 父节点的索引
int parent(int root) {
    return root / 2;
}
// 左孩子的索引
int left(int root) {
    return root * 2;
}
// 右孩子的索引
int right(int root) {
    return root * 2 + 1;
}

在这里插入图片描述
举例: 看 R 节点,其父节点在数组中的位置是 3/2=1,其左孩子节点在数组中的位置是 32=6(O),右孩子节点在数组中的位置是 32+1 = 7(A)

  • 分类:
    • 最大堆:每个节点都大于等于它的两个子节点
    • 最小堆:每个节点都小于等于它的子节点

3.2 优先级队列

重要性质: 插入或删除元素的时候,元素会自动排序——底层就是二叉堆

  • 主要应用:插入一个元素和删除最大元素(最大堆)或删除最小元素(最小堆)
public class MaxPQ
    <Key extends Comparable<Key>> {
    // 存储元素的数组
    private Key[] pq;
    // 当前 Priority Queue 中的元素个数
    private int size = 0;

    public MaxPQ(int cap) {
        // 索引 0 不用,所以多分配一个空间
        pq = (Key[]) new Comparable[cap + 1];
    }

    /* 返回当前队列中最大元素 */
    public Key max() {
        return pq[1];
    }

    /* 插入元素 e */
    public void insert(Key e) {...}

    /* 删除并返回当前队列中最大元素 */
    public Key delMax() {...}

    /* 上浮第 x 个元素,以维护最大堆性质 */
    private void swim(int x) {...}

    /* 下沉第 x 个元素,以维护最大堆性质 */
    private void sink(int x) {...}

    /* 交换数组的两个元素 */
    private void swap(int i, int j) {
        Key temp = pq[i];
        pq[i] = pq[j];
        pq[j] = temp;
    }

    /* pq[i] 是否比 pq[j] 小? */
    private boolean less(int i, int j) {
        return pq[i].compareTo(pq[j]) < 0;
    }

    /* 还有 left, right, parent 三个方法 */
}

下称: 若某个节点A比它的子节点(中的一个)小,那么A就不配做父节点,应该让A下去,让更大的那个节点上来
上浮: 若某个节点B比它的父节点大,那么B就不应该做子节点 ,应该让B上去,让父节点下来

注: 操作只会在堆底和堆顶进行,堆底上浮,堆顶下沉

上浮代码: 利用while循环,让x不断上去

public class MaxPQ <Key extends Comparable<Key>> {
    // 为了节约篇幅,省略上文给出的代码部分...

    private void swim(int x) {
        // 如果浮到堆顶,就不能再上浮了
        while (x > 1 && less(parent(x), x)) {
            // 如果第 x 个元素比上层大
            // 将 x 换上去
            swap(parent(x), x);
            x = parent(x);
        }
    }
}

在这里插入图片描述
举例: x 是子节点,P和T互换,x = p,T=p.parent(x),所以再令x = T,去看STR这个枝干——交换之后,继续上浮,让x始终做上一层的子节点,去与其新的父节点比较

下沉代码:

public class MaxPQ <Key extends Comparable<Key>> {
    // 为了节约篇幅,省略上文给出的代码部分...

    private void sink(int x) {
        // 如果沉到堆底,就沉不下去了
        while (left(x) <= size) {
            // 先假设左边节点较大
            int max = left(x);
            // 如果右边节点存在,比一下大小
            if (right(x) <= size && less(max, right(x)))
                max = right(x);
            // 结点 x 比俩孩子都大,就不必下沉了
            if (less(max, x)) break;
            // 否则,不符合最大堆的结构,下沉 x 结点
            swap(x, max);
            x = max;
        }
    }
}

插入insert: 将要插入的元素添加到堆底的最后,然后让其上浮到正确位置

public class MaxPQ <Key extends Comparable<Key>> {
    // 为了节约篇幅,省略上文给出的代码部分...

    public void insert(Key e) {
        size++;
        // 先把新元素加到最后
        pq[size] = e;
        // 然后让它上浮到正确的位置
        swim(size);
    }
}

delMax: 先把堆定元素和堆底最后的元素B对调,然后删除A,让B下沉到正确位置

public class MaxPQ <Key extends Comparable<Key>> {
    // 为了节约篇幅,省略上文给出的代码部分...

    public Key delMax() {
        // 最大堆的堆顶就是最大元素
        Key max = pq[1];
        // 把这个最大元素换到最后,删除之
        swap(1, size);
        pq[size] = null;
        size--;
        // 让 pq[1] 下沉到正确位置
        sink(1);
        return max;
    }
}

注: 插入和删除元素的时间复杂度为 O(logK)

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明前大奏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值