数据结构与算法 复习记录(二)

单链表–老师讲的

单链表

特点分析
  1. 若有尾结点, 尾插尾删是O(1)

  2. 单链表 设计中,头结点(Head Node)通常不是有效元素,而是一个 哨兵节点(dummy node),主要用于简化链表操作,比如插入、删除等。

  3. 分布式系统的CAP定理: 在 分布式系统 中,无法同时满足以下三个特性

    C: 一致性:所有节点在同一时间看到的数据是一致的(强一致性)。
    A: 可用性:系统始终能响应请求,不会返回错误或超时。
    P: 分区合理性 :即使网络分区(节点间通信中断),系统仍能继续运行。

    由于网络分区(P)在分布式系统中不可避免(如网络故障、机器宕机),实际选择通常是 CP 或 AP

    为什么无法同时满足 CAP?

    • 网络分区(P)不可避免:分布式系统中,节点间通信可能失败(网络故障、机器宕机)。
    • C 和 A 冲突
      • 如果要保证 一致性(C),在分区发生时必须阻塞写入或读取(牺牲可用性)。
      • 如果要保证 可用性(A),在分区发生时必须允许不一致的读写(牺牲一致性)。
  4. 计算机 里 很少有最好的, 只有适合的

  5. 搜索上, 有序地数组是 比 链表高的 – 二分

  6. 怎么选择数组和链表?

    下标访问/随机访问多, 搜索多(有序), 用数组

    增删多, 用链表

  7. 单链表接口实现:

    自己实现链表: 写 链表接口-- 头插, 尾插, 删除一个节点, 删除节点(有重复), 搜索, 打印

    // 单链表代码实现
    // 节点类型---可以放进类里,详见单向循环链表
    struct Node
    {
        Node(int data = 0) :data_(data), next_(nullptr) {}
        int data_;
        Node* next_;
    };
    
    class Clink
    {
    public:
        Clink()
        {
            // 给head_初始化指向头节点
            head_ = new Node();
        }
        ~Clink()
        {
            // 节点的释放
            Node* p = head_;
            while (p != nullptr)
            {
                head_ = head_->next_;
                delete p;
                p = head_;
            }
            head_ = nullptr;
        }
    
    public:
        // 链表尾插法 O(n)   head_:头节点    tail_:尾节点
        void InsertTail(int val)
        {
            // 先找到当前链表的末尾节点
            Node* p = head_;
            while (p->next_ != nullptr)
            {
                p = p->next_;
            }
    
            // 生成新节点
            Node* node = new Node(val);
            // 把新节点挂在尾节点的后面
            p->next_ = node;
        }
        // 链表的头插法  O(1)
        void InsertHead(int val)
        {
            Node* node = new Node(val);
            node->next_ = head_->next_;
            head_->next_ = node;
        }
        // 链表节点的删除
        void Remove(int val)
        {
            Node* q = head_;
            Node* p = head_->next_;
    
            while (p != nullptr)
            {
                if (p->data_ == val)
                {
                    // 删除一个节点本身的操作是O(1)
                    q->next_ = p->next_;
                    delete p;
                    return;
                }
                else
                {
                    q = p;
                    p = p->next_;
                }
            }
        }
        // 删除多个节点
        void RemoveAll(int val)
        {
            Node* q = head_;
            Node* p = head_->next_;
    
            while (p != nullptr)
            {
                if (p->data_ == val)
                {
                    q->next_ = p->next_;
                    delete p;
                    // 对指针p进行重置
                    p = q->next_;
                }
                else
                {
                    q = p;
                    p = p->next_;
                }
            }
        }
        // 搜索 list O(n) 数组的搜索  下表访问/随机访问arr[i]O(1)  搜索O(n)
        bool Find(int val)
        {
            Node* p = head_->next_;
            while (p != nullptr)
            {
                if (p->data_ == val)
                {
                    return true;
                }
                else
                {
                    p = p->next_;
                }
            }
            return false;
        }
        // 链表打印
        void Show()
        {
            Node* p = head_->next_;
            while (p != nullptr)
            {
                cout << p->data_ << " ";
                p = p->next_;
            }
            cout << endl;
        }
    private:
        Node* head_; // 指向链表的头节点
    };
    

单链表-复习

new Node()new Node 的区别

表达式行为
new Node()调用 Node 的构造函数,并进行值初始化data_ 会被初始化为 0next_ 初始化为 nullptr)。
new Node调用 Node 的构造函数,但不进行值初始化(如果 Node 是 POD 类型,可能会有未定义值,但这里 Node 有构造函数,所以行为一致)。

POD = 像 C 结构体一样简单的数据类型。

何为POD

  1. 定义

    • 简单的数据类型,兼容 C 语言(如 intfloat、不含虚函数的 struct)。
    • 标准布局(成员排列紧凑) + 平凡构造/析构(无自定义构造/拷贝/析构)。
  2. 特点

    • 可直接 memcpy 复制、二进制存储(如网络传输)。

    • 示例:

      cpp

      复制

      struct Point { int x; int y; };  // POD
      
  3. 非 POD 例子

    • 含虚函数、自定义构造函数、std::string 等(不可直接内存操作)。
  4. 用途

    • 底层操作(序列化、C 交互)时需保证类型是 POD。

自己练习:

自己实现链表: 写 链表接口-- 头插, 尾插, 删除一个节点, 删除节点(有重复), 搜索, 打印

#include <iostream>
using namespace std;

// 先定义 节点
struct Node
{
	Node(int data = 0)
		:data_(data)
		, next_(nullptr)
	{ }

	int data_;
	Node* next_;
};

class Clink
{
public:
	Clink()
	{
		head_ = new Node();
	}
	~Clink()
	{
		Node* p = head_;
		while (p != nullptr)
		{
			head_ = head_->next_;
			delete p;
			p = head_;
		}
		head_ = nullptr; // 防止野指针
	}

	//头插
	void InsertHead(int data)
	{
		Node* node = new Node(data);
		node->next_ = head_->next_;
		head_->next_ = node;
	}

	//尾插
	void InsertTail(int data)
	{
		Node* p = head_;
		while (p->next_ != nullptr) 
		{
			p = p->next_;
		}
		// 此时p 就在 要插入的位置
		Node* node = new Node(data);
		p->next_ = node;
	}

	//删除一个----先要查找在不在
	void Remove(int data)
	{
		Node* p = head_;
		Node* q = head_->next_;
		if (Find(data))
		{
			while (q != nullptr)
			{
				if (q->data_ == data)
				{
					p->next_ = q->next_;
					delete q;
					q = nullptr;
					return;
				}
				else
				{
					p = p->next_;
					q = q->next_;
				}
			}
			
		}
		

		
	}

	//有重复值的 删除
	void RemoveAll(int data)
	{
		Node* p = head_;
		Node* q = head_->next_;
		if (Find(data))
		{
			while (q != nullptr)
			{
				if (q->data_ == data)
				{
					p->next_ = q->next_;
					delete q;
					q = p->next_;
				}
				else
				{
					p = p->next_;
					q = q->next_;
				}
			}

		}

	}

	//查找
	bool Find(int data)
	{
		Node* p = head_;
		while (p != nullptr)
		{
			if (p->data_ == data)
			{
				return true;
			}
			else
			{
				p = p->next_;
			}
		}
		return false;
	}

	//打印
	void show()
	{
		Node* p = head_->next_;
		while (p != nullptr)
		{
			cout << p->data_ << " ";
			p = p->next_;
		}
		cout << endl;
	}

private:
	Node* head_;
	
};



int main()
{
	Clink clink;
	clink.InsertHead(1);
	clink.InsertHead(2);
	clink.InsertHead(3);
	clink.InsertHead(4);

	clink.show(); // 4 3 2 1
	
	clink.InsertTail(5);
	clink.show(); // 4 3 2 1 5

	clink.Remove(4);
	clink.show(); //3 2 1 5

	clink.InsertTail(2);
	clink.show(); // 3 2 1 5 2
	clink.RemoveAll(2);
	clink.show(); // 3 1 5

}
笔试面试
单链表逆序–后面的无法往前走!! 双指针就不行了

单链表逆序—头插

一般单链表第一个head节点, 不是一个有效的节点—使用这个特性

//核心

q=p->next;
p->next=head->next;
head->next=p;
p=q;
// 单链表逆序---可以从第二个开始, 节省了第一个插入
void ReverseLink(Clink &link)
{
    Node* head = link.head_;
    Node* p = head->next_;
    if (p == nullptr)
    {
        return;
    }

    head->next_ = nullptr;

    while (p != nullptr)
    {
        Node* q = p->next_;

        // p指针指向的节点进行头插
        p->next_ = head->next_;
        head->next_ = p;

        p = q;
    }
}
单链表倒数第k个节点—一定要多画图, 一定不要让程序挂掉

首先并不知道 链表有多少节点, 那么这个 倒数第k个 就无法一个指针 直接从头走了

而 如果 先逆序, 再正着找, 步骤多
使用数组, 更是 复杂度提高了
本题还是双指针(快慢指针), 两个指针直接 差着 k-1个节点, 这样当快指针到达尾部, 慢的指针指向就是 这个 倒数第k个节点

// 求倒数第k个节点的值
bool GetLaskKNode(Clink& link, int k, int& val)//有成功又失败,一般是返回bool, 使用引用带回结果
{
    Node* head = link.head_;
    Node* pre = head;
    Node* p = head;

    if (k < 1)//不判断,程序会有机会崩溃, 访问了 空指针
    {
        return false;
    }

    for (int i = 0; i < k; i++)
    {
        p = p->next_;
        if (p == nullptr) // 防止k>元素数量
        {
            return false;
        }
    }

    // pre在头节点,p在正数第k个节点
    while (p != nullptr)
    {
        pre = pre->next_;
        p = p->next_;
    }

    val = pre->data_;
    return true;
}
合并两个有序地链表–双指针应用在两个链表上

实际是3指针, 两个用于遍历, 一个用于插入

// 合并两个有序的单链表
void MergeLink(Clink& link1, Clink& link2)
{
    Node* p = link1.head_->next_;
    Node* q = link2.head_->next_;
    Node* last = link1.head_;
    link2.head_->next_ = nullptr;

    while (p != nullptr && q != nullptr)
    {
        if (p->data_ < q->data_)
        {
            last->next_ = p;
            p = p->next_;
            last = last->next_;
        }
        else
        {
            last->next_ = q;
            q = q->next_;
            last = last->next_;
        }
    }

    if (p != nullptr)
    {
        last->next_ = p;
    }
    else
    {
        last->next_ = q;
    }
}
判断单链表是否有环? 求环 的 入口节点–超经典问题

环: 即找不到末尾阶段了, 没有末尾节点是 nullptr

  1. 方法1:主要是 识别 链表地址 重不重复,使用一个数组 记录 节点的地址—不建议开辟额外的内存

  2. 尽量原地解决! 双指针的应用, 快慢指针, 环, 能套一圈, 就是有环了, 慢指针一次跑一个, 快指针一次跑两个, 快的先跑到空, 就是没环, 当二者能遇上, 那么就有环了

  3. b的挖坑: 快指针跑的快,刚好把慢的越过去了, 会不会每次 都会跳过了?(会越过去的前提,是快慢已经相等了, 才会跨过)

  4. 那使用b的方法, 怎么找 环的入口和环的入口的前一个呢?

    使用数学推导: 结论: 在再次相遇的地方, 定义一个指针,开头处再定义一个,两个一块走(a=x,这个a实际比图中的多了一步,注意分析), 相遇的地方, 就是 环入口

    在这里插入图片描述

// 判断单链表是否存在环,存在返回环的入口节点
bool IsLinkHasCircle(Node *head, int& val)
{
    Node *fast = head;
    Node* slow = head;

    while (fast != nullptr && fast->next_ != nullptr)//两步都得判断
    {
        slow = slow->next_;
        fast = fast->next_->next_;

        if (slow == fast)
        {
            // 快慢指针再次遇见,链表存在环
            fast = head;  // 开始找入口, 重用指针
            while (fast != slow)
            {
                slow = slow->next_;
                fast = fast->next_;
            }
            val = slow->data_;
            return true;
        }
    }
    return false;
}
判断两个单链表是否相交, 返回相交节点的值

相交的定义:两个单链表从某个节点开始,它们之后的节点完全相同,即从某个节点开始,两个链表共享相同的后续节点。

传统解法: 用数组 存一个链表地址, 找重复的第二个链表
**原地解法: **双指针—原地解法通过计算两个链表的长度差,让长链表的指针先走一段距离,再同步遍历,直到找到相交节点或返回 nullptr

// 判断两个单链表是否相交,如果相交,返回相交节点的值
bool IsLinkHasMerge(Node* head1, Node* head2, int& val)
{
    int cnt1 = 0, cnt2 = 0;
    Node* p = head1->next_;
    Node* q = head2->next_;

    while (p != nullptr)
    {
        cnt1++;
        p = p->next_;
    }

    while (q != nullptr)
    {
        cnt2++;
        q = q->next_;
    }

    p = head1;
    q = head2;
    if (cnt1 > cnt2)
    {
        // 第一个链表长
        int offset = cnt1 - cnt2;
        while (offset-- > 0) 
        {
            p = p->next_;
        }
    }
    else
    {
        // 第二个链表长
        int offset = cnt2 - cnt1;
        while (offset-- > 0)
        {
            q = q->next_;
        }
    }

    while (p != nullptr && q != nullptr)
    {
        if (p == q)
        {
            val = p->data_;
            return true;
        }
        p = p->next_;
        q = q->next_;
    }

    return false;
}
力扣-19:删除倒数第n个

此题的坑点, 没有第一个哨兵节点, 可以加一个临时的哨兵, 使得逻辑更简单

struct ListNode *removeNthFromEnd(struct ListNode *head, int n)
{
    // 在函数内部加一个哨兵节点, 问题就很简单, 思考的地方也会很少
    struct ListNode head_; // 在栈区, 仅这个函数用
    head_.next = head;

    struct ListNode *p = &head_; // c里面访问结构体, struct不能省略
    struct ListNode *q = &head_;
    for (int i = 0; i < n; ++i) // 找要删除的前一个, 差值要正确
    {
        if (p->next == NULL)
            return head;
        p = p->next;
    }
    while (p->next != NULL)
    {
        p = p->next;
        q = q->next;
    }

    struct ListNode *del = q->next;
    q->next = q->next->next;
    free(del);

    return head_.next;
}
力扣-61:旋转链表

后面的依次使用头插—不能一个一个插, 单链表无法找到前一个

仔细观察, 就是把后面的整体做一个前插

//自己的
	//这种会出错, k=size时,会导致移动到最后访问越界
    //int m = k;
    //if(size<k) m = k%size;


struct ListNode* rotateRight(struct ListNode* head, int k) {
            struct ListNode *p = head;
    struct ListNode *q = head;
    int size = 1;

    
    if (p==NULL || p->next == NULL)
    {
        return head;
    }
    while (p->next != NULL)
    {
        p = p->next;
        ++size;
    }

    p = head;

    int m = k%size;
    for (int i = 0; i < m; ++i)
    {
        p = p->next;
    }
    while (p->next != NULL)
    {
        p = p->next;
        q = q->next;
    }
    p->next = head;
    head = q->next;
    q->next = NULL;
    return head;
}
//老师的 空间复杂度O(1)
struct ListNode *rotateRight(struct ListNode *head, int k)
{
    struct ListNode *p = head;
    struct ListNode *q = head;
    int number = 0;
    if (p==NULL || k==0)
    {
        return head;
    }
    //O(n)
    for(struct ListNode *m=head;m!=NULL;m=m->next)
    {
        ++number;
    }
    
    k=k%number;
    if(k==0)  // 这里加一个, 避免如冗余
    {
        return head;
    }
    
    //O(n)
    for(int i=0;i<k;++i)
    {
        p=p->next;
    }

    while (p->next != NULL)
    {
        p = p->next;
        q = q->next;
    }
    p->next = head;
    head = q->next;
    q->next = NULL;
    return head;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值