数据结构与算法基础知识点和例题

数据结构

基础知识

1.数据结构与算法是什么

数据结构是个体的存储个体的关系存储;算法是对存储数据的操作

2.衡量算法的标准

时间复杂度(程序执行的次数而非时间)、空间复杂度(执行过程中占用的最大内存)、难易程度和健壮性。

3.结构体struct

struct Student{
	char name[100];
	int age;
	int *sex;
};//创建了一个结构体类型struct student,该类型包括三种数据类型。
struct Student p;//定义了一个结构体变量p。
struct Student *q = &p;//定义了一个结构体指针变量q。

//通常会将typedef和结构体集合->typedef 已有类型 自定义类型名
typedef struct Student{
	char name[100];
	int age;
	int *sex;
}ST;
ST p;//定义了一个结构体变量p。
ST *q = &p;//定义了一个结构体指针变量q。

//或结构体指针类型
typedef struct Student{
	char name[100];
	int age;
	int *sex;
} *ST;//ST等价于struct Student *。这里可以理解为 typedef int *ST,
struct Student p;
ST q = &st;

//或结合
typedef struct Student{
	char name[100];
	int age;
	int *sex;
} *ST1,ST2;//ST1等价于struct Student *, ST2等价于struct Student。
	   //这里可以理解为 typedef int *ST1,ST2(或者理解为int *ST1,ST2;)
ST2 p;
ST1 q = &st;

4.malloc函数

(1)为什么要强制转换

答:因为malloc函数的返回值是void类型指针(void类型地址),即返回分配内存的起始地址,该地址上存储的数据是未定义类型的。
强制转换之后,该数据类型有两个属性。一个是地址,就是在内存中这个变量从哪里开始存放;另一个是长度,从这个地址开始,后面还有几个字节是属于当前这个变量。

(2)要判断malloc有没有成功开辟内存空间

当malloc函数成功开辟内存后,会返回对应的指针。要是内存空间不够大,内存开启失败,会返回NULL.

int *p = (int*)malloc(sizeof(int));
if(p == NULL){
	perror("malloc failed");
	exit(1);
}

(3)要对数据进行初始化
malloc函数只是负责开辟内存空间。并没有对数据进行初始化。我们可以使用memset函数进行初始化.

(4)内存空间使用完成后,要释放空间
堆区开辟的内存空间,编译器是不会自动回收。需要开发者自己回收。可以使用free()。
注意:使用free函数释放结构体的时候,并没有释放结构体内的指针。结构体内的指针要单独free。

struct person{
	int age;
	char *name;
};
int main(){
	struct person *p = (struct person*)malloc(sizeof(struct person));
	if(p == NULL){
		perror("malloc");
		exit(1);
	}
	memset(p,0,sizeof(struct person));
	p->age = 18;
	p->name = malloc(sizeof(char) * 20);
	char *name = "jerry";
	strcpy(p->name,name);
	
	printf("p->name:\t%s\n",p->name);
	
	free(p->name);// 这里要单独将指针free掉
	free(p);
}

参考资料:https://blog.csdn.net/weixin_49146002/article/details/128314606

线性表(线性结构)

线性表list(抽象数据类型)
它只是一个抽象的数学结构,并不具有具体的物理形式,线性表需要通过其它具有物理形式的数据结构(数组、链表、哈希表、栈或队列)来实现。

非线性表(非线性结构)

树,图

数组

定义

数组是一种线性表,是具体的数据结构。它是存放在连续内存空间上相同类型数据的集合。
它是一种连续的线性表

例题

1.二分查找——二分法https://leetcode.cn/problems/binary-search/)
注意:
二分查找时,我们往往会见到以下三种方法来求mid中间值
1.正常思路
mid = (left + right) / 2;
2.防溢出写法
mid = left + (right - left)/2;
3.位运算 也是防溢出 无符号位元素符的优先级较低,所以括起来
mid = left + ((right - left)>>2);

一般我们都是定义左边界(left)和右边界(right)都使用 int 类型,如果 left 和 right 足够大,第一种mid = (left + right)/2,可能会由于 left+right 导致 int 数据类型越界。

2.移除元素——双指针法https://leetcode.cn/problems/remove-element/)
双指针法:通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
双指针法一定要明白两个指针各自的功能。比如在移除元素中,快指针负责指向非目标值(如果遇到目标值就自加1),慢指针指向目标数组的最后一个元素。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int slowIndex = 0;
        for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
            if (val != nums[fastIndex]) {//快指针只能指向非目标值
                nums[slowIndex++] = nums[fastIndex];
            }
        }
        return slowIndex;//慢指针指向目标数组的最后一个元素,即数组长度
    }
};

3.长度最小的子数组(最小窗口)——滑动窗口法https://leetcode.cn/problems/minimum-size-subarray-sum/)
滑动窗口包括最小窗口和最大窗口(https://leetcode.cn/problems/fruit-into-baskets/)
fast控制滑动窗口的右边
slow控制滑动窗口的左边

class Solution {
public:
    int minSubArrayLen(int s, vector<int>& nums) {
        int n = nums.size();
        if(n == 0)
        {
            return 0;
        }
        int slow = 0;
        int fast = 0;//一头一尾
        int sum = 0;
        int ans = INT_MAX;
        while(fast < n)
        {
            sum += nums[fast];
            while(sum >= s)
            {
                ans = min(ans, fast - slow + 1);
                sum -= nums[slow];
                slow++;
            }
            fast++;
        }
        return ans == INT_MAX ? 0 : ans;
    }

};

4.螺旋矩阵——模拟过程https://leetcode.cn/problems/spiral-matrix-ii/)

链表

定义

离散、指针、线性

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向NULL(空指针的意思)。

链表的入口节点称为链表的头结点也就是head指针。
链表的出口节点称为链表的尾结点也就是NULL指针。

它是一种离散的线性表。

想要搞清楚链表,需要理解:
首节点:第一个有效节点
尾节点:最后一个有效节点
头结点(为了方便链表的后续操作,添加一个虚拟的节点。它不保存有效数据。)
头指针:指向头结点的指针变量
尾指针:指向尾节点的指针变量
image

// 单链表
struct Node {
    int val;  // 数据域
    struct Node *pNext;  //指针域
    Node(int x) : val(x), pNext(NULL) {}  // 节点的构造函数(C++会默认生成)
};

//或者使用typedef
typedef struct Node{
	int val;//数据域
	struct Node *pNext;//指针域
}NODE,*PNODE;//NODE等价于struct Node,*PNODE等价于struct Node *

类型

单链表,双链表和循环链表(解决约瑟夫环问题???)

双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。

循环链表:链表首尾相连

操作

1.移除元素(https://leetcode.cn/problems/remove-linked-list-elements/)
2.设计链表(https://leetcode.cn/problems/design-linked-list/submissions/)
3.判断链表是否有环()
可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。然后在相遇节点和首节点设置指针,两个指针一起移动,相遇处就是环形入口的节点。
也可以用哈希表遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。

(1)创建链表


PNODE create_list(void)
{
	int len;  //用来存放有效节点的个数
	int i;
	int val; //用来临时存放用户输入的结点的值

	//分配了一个不存放有效数据的头结点
	PNODE pHead = (PNODE)malloc(sizeof(NODE));
	if (NULL == pHead)
	{
		printf("分配失败, 程序终止!\n");
		exit(-1);
	}
	PNODE pTail = pHead;
	pTail->pNext = NULL;

	printf("请输入您需要生成的链表节点的个数: len = ");
	scanf("%d", &len);
	
	for (i=0; i<len; ++i)
	{
		printf("请输入第%d个节点的值: ", i+1);
		scanf("%d", &val);
		
		PNODE pNew = (PNODE)malloc(sizeof(NODE));
		if (NULL == pNew)
		{
			printf("分配失败, 程序终止!\n");
			exit(-1);
		}
		pNew->data = val;
		pTail->pNext = pNew;
		pNew->pNext = NULL;
		pTail = pNew;
	}
	
	return pHead;
}

(2)遍历链表

void traverse_list(PNODE pHead)
{
	PNODE p = pHead->pNext;

	while (NULL != p)
	{
		printf("%d  ", p->data);
		p = p->pNext;
	}
	printf("\n");
	
	return;
}

(3)求链表长度

int length_list(PNODE pHead)
{
	PNODE p = pHead->pNext;
	int len = 0;

	while (NULL != p)
	{
		++len;
		p = p->pNext;
	}

	return len;
}

(4)判断链表是否为空

bool is_empty(PNODE pHead)
{
	if (NULL == pHead->pNext)
		return true;
	else
		return false;
}

(5)插入节点
两种方式:
image
第一种:r=p->pNext;p->pNext=q;q->next=r;
第二种:q->pNext = p->pNext;p->pNext=q;


//在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
bool insert_list(PNODE pHead, int pos, int val)
{
	int i = 0;
	PNODE p = pHead;

	while (NULL!=p && i<pos-1)
	{
		p = p->pNext;
		++i;
	}

	if (i>pos-1 || NULL==p)
		return false;

	//如果程序能执行到这一行说明p已经指向了第pos-1个结点,但第pos-1个节点是否存在无所谓
	//分配新的结点
	PNODE pNew = (PNODE)malloc(sizeof(NODE));
	if (NULL == pNew)
	{
		printf("动态分配内存失败!\n");
		exit(-1);
	}
	pNew->data = val;
	
	//将新的结点存入p节点的后面
	PNODE q = p->pNext;
	p->pNext = pNew;
	pNew->pNext = q;

	return true;
}

(6)删除节点
方法一:在原有的链表上操作
思路:
image
代码实现:


bool delete_list(PNODE pHead, int pos, int * pVal)
{
	int i = 0;
	PNODE p = pHead;

	while (NULL!=p->pNext && i<pos-1)
	{
		p = p->pNext;
		++i;
	}

	if (i>pos-1 || NULL==p->pNext)
		return false;
	
	//如果程序能执行到这一行说明p已经指向了第pos-1个结点,并且第pos个节点是存在的
	PNODE q = p->pNext;  //q指向待删除的结点
	*pVal = q->data;  

	//删除p节点后面的结点
	p->pNext = p->pNext->pNext;
	
	//释放q所指向的节点所占的内存
	free(q);
	q = NULL;
	
	return true;

}

或者,以力扣的删除链表为例:
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
		
        while(NULL != head && head->val == val)//处理首节点
        {
            struct ListNode * tmp = head;
            head = head->next;
            delete tmp;
        }
		
        struct ListNode * cur = head;//cur表示当前访问到第几个
        while(NULL != cur && NULL != cur->next)//处理非首节点
        {
            if(cur->next->val == val)
            {
                struct ListNode * tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else//删除之后不用移动,不删除才移动
            {
                cur = cur->next;
            }
        }
        return head;
    }
};

方法二:添加虚拟节点

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        //方法二:添加虚拟节点,首节点和非首节点一起处理
        struct ListNode * dummyNode = new ListNode();//struct ListNode * dummyNode = head;注意二者的区别
        dummyNode->next = head;//虚拟头结点指向首节点
        struct ListNode * cur = dummyNode;

        while(NULL != cur->next)
        {
            if(cur->next->val == val)
            {
                struct ListNode * tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else
            {
                cur = cur->next;
            }
        }
        head = dummyNode->next;
        delete dummyNode;
        return head;
    }
};

(7)整体代码

# include <stdio.h>
# include <malloc.h>
# include <stdlib.h>

typedef struct Node
{
	int data; //数据域
	struct Node * pNext; //指针域
}NODE, *PNODE; //NODE等价于struct Node    PNODE等价于struct Node *

//函数声明
PNODE create_list(void);  //创建链表
void traverse_list(PNODE pHead);  //遍历链表
bool is_empty(PNODE pHead);  //判断链表是否为空
int length_list(PNODE);  //求链表长度
bool insert_list(PNODE pHead, int pos, int val);  //在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
bool delete_list(PNODE pHead, int pos, int * pVal);  //删除链表第pos个节点,并将删除的结点的值存入pVal所指向的变量中,  并且pos的值是从1开始
void sort_list(PNODE);  //对链表进行排序


int main(void)
{
	PNODE pHead = NULL; //等价于 struct Node * pHead = NULL;
	int val;

	pHead = create_list();  //create_list()功能:创建一个非循环单链表,并将该链表的头结点的地址付给pHead
	traverse_list(pHead);
	
	//insert_list(pHead, -4, 33);
	if ( delete_list(pHead, 4, &val) )
	{
		printf("删除成功,您删除的元素是: %d\n", val);
	}
	else
	{
		printf("删除失败!您删除的元素不存在!\n");
	}

	traverse_list(pHead);
	
	//int len = length_list(pHead);
	//printf("链表的长度是%d\n", len);

	//sort_list(pHead);
	//traverse_list(pHead);
	
/*	if ( is_empty(pHead) )
		printf("链表为空!\n");
	else
		printf("链表不空!\n");
*/
	return 0;
}

PNODE create_list(void)
{
	int len;  //用来存放有效节点的个数
	int i;
	int val; //用来临时存放用户输入的结点的值

	//分配了一个不存放有效数据的头结点
	PNODE pHead = (PNODE)malloc(sizeof(NODE));
	if (NULL == pHead)
	{
		printf("分配失败, 程序终止!\n");
		exit(-1);
	}
	PNODE pTail = pHead;
	pTail->pNext = NULL;

	printf("请输入您需要生成的链表节点的个数: len = ");
	scanf("%d", &len);
	
	for (i=0; i<len; ++i)
	{
		printf("请输入第%d个节点的值: ", i+1);
		scanf("%d", &val);
		
		PNODE pNew = (PNODE)malloc(sizeof(NODE));
		if (NULL == pNew)
		{
			printf("分配失败, 程序终止!\n");
			exit(-1);
		}
		pNew->data = val;
		pTail->pNext = pNew;
		pNew->pNext = NULL;
		pTail = pNew;
	}
	
	return pHead;
}

void traverse_list(PNODE pHead)
{
	PNODE p = pHead->pNext;

	while (NULL != p)
	{
		printf("%d  ", p->data);
		p = p->pNext;
	}
	printf("\n");
	
	return;
}

bool is_empty(PNODE pHead)
{
	if (NULL == pHead->pNext)
		return true;
	else
		return false;
}

int length_list(PNODE pHead)
{
	PNODE p = pHead->pNext;
	int len = 0;

	while (NULL != p)
	{
		++len;
		p = p->pNext;
	}

	return len;
}

void sort_list(PNODE pHead)
{
	int i, j, t;
	int len = length_list(pHead);
	PNODE p, q;
	
	for (i=0,p=pHead->pNext; i<len-1; ++i,p=p->pNext)
	{
		for (j=i+1,q=p->pNext; j<len; ++j,q=q->pNext)
		{
			if (p->data > q->data)  //类似于数组中的:  a[i] > a[j]
			{
				t = p->data;//类似于数组中的:  t = a[i];
				p->data = q->data; //类似于数组中的:  a[i] = a[j];
				q->data = t; //类似于数组中的:  a[j] = t;
			}
		}
	}

	return;
}

//在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
bool insert_list(PNODE pHead, int pos, int val)
{
	int i = 0;
	PNODE p = pHead;

	while (NULL!=p && i<pos-1)
	{
		p = p->pNext;
		++i;
	}

	if (i>pos-1 || NULL==p)
		return false;

	//如果程序能执行到这一行说明p已经指向了第pos-1个结点,但第pos-1个节点是否存在无所谓
	//分配新的结点
	PNODE pNew = (PNODE)malloc(sizeof(NODE));
	if (NULL == pNew)
	{
		printf("动态分配内存失败!\n");
		exit(-1);
	}
	pNew->data = val;
	
	//将新的结点存入p节点的后面
	PNODE q = p->pNext;
	p->pNext = pNew;
	pNew->pNext = q;

	return true;
}


bool delete_list(PNODE pHead, int pos, int * pVal)
{
	int i = 0;
	PNODE p = pHead;

	while (NULL!=p->pNext && i<pos-1)
	{
		p = p->pNext;
		++i;
	}

	if (i>pos-1 || NULL==p->pNext)
		return false;
	
	//如果程序能执行到这一行说明p已经指向了第pos-1个结点,并且第pos个节点是存在的
	PNODE q = p->pNext;  //q指向待删除的结点
	*pVal = q->data;  

	//删除p节点后面的结点
	p->pNext = p->pNext->pNext;
	
	//释放q所指向的节点所占的内存
	free(q);
	q = NULL;
	
	return true;
}

性能

例题

两数相加(https://leetcode.cn/problems/add-two-numbers/)

哈希表

定义

哈希表(散列表)是根据关键码的值而直接进行访问的数据结构。哈希表都是用来快速判断一个元素是否出现集合里

当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

类型

array数组
set集合
map映射

哈希碰撞

所谓哈希(hash),就是将不同的输入映射成独一无二的、固定长度的值(又称"哈希值")。它是最常见的软件运算之一。如果不同的输入得到了同一个哈希值,就发生了"哈希碰撞"(collision)。
解决哈希碰撞的方法
方法一:拉链法,即发生冲突的元素都被存储在链表中
方法二:线性探测法,即找下一个空位存放冲突的元素(注意哈希表大小一定要大于数据大小

例题

两数之和(https://leetcode.cn/problems/two-sum/)

定义

栈是先进后出。栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以栈的底层实现可以是vector,deque,list 都是可以,即STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。
我们也可以指定vector为栈的底层实现,初始化语句如下:

std::stack<int, std::vector<int> > third;  // 使用vector为底层容器的栈

通常栈顶为Top,栈底为Buttom。
注意:静态分配到栈(例如:定义一个局部变量),动态分配到堆(例如:malloc函数)

类型

静态栈(内核为数组)
动态栈(内核为链表)(主要)

操作

1.用栈实现队列
思路:将一个栈当作输入栈,用于压入push传入的数据;另一个栈当作输出栈,用于pop和
peek操作。每次pop或peek时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈,这样输出栈从栈顶往栈底的顺序就是队列从队首往队尾的顺序(实现先入先出)。

class MyQueue {
private:
    stack<int> inStack, outStack;

    void in2out() {
        while (!inStack.empty()) {
            outStack.push(inStack.top());
            inStack.pop();
        }
    }

public:
    MyQueue() {}

    void push(int x) {
        inStack.push(x);
    }

    int pop() {
        if (outStack.empty()) {
            in2out();
        }
        int x = outStack.top();
        outStack.pop();
        return x;
    }

    int peek() {
        if (outStack.empty()) {
            in2out();
        }
        return outStack.top();
    }

    bool empty() {
        return inStack.empty() && outStack.empty();
    }
};

2.有效的括号
用哈希表和栈:
当我们遇到一个右括号时,我们需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串 s 无效,返回 False。为了快速判断括号的类型,我们可以使用哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。
在遍历结束后,如果栈中没有左括号,说明我们将字符串s中的所有左括号闭合,返回 True,否则返回 False。

压栈(入栈)
遍历
出栈
清空

# include <stdio.h>
# include <malloc.h>
# include <stdlib.h>

typedef struct Node
{
	int data;
	struct Node * pNext;
}NODE, * PNODE;

typedef struct Stack
{
	PNODE pTop;
	PNODE pBottom;
}STACK, * PSTACK;  //PSTACK 等价于 struct STACK *

void init(PSTACK);
void push(PSTACK, int );
void traverse(PSTACK);
bool pop(PSTACK, int *);
void clear(PSTACK pS);

int main(void)
{
	STACK S;  //STACK 等价于 struct Stack
	int val;

	init(&S);  //目的是造出一个空栈
	push(&S, 1); //压栈
	push(&S, 2);
	push(&S, 3);
	push(&S, 4);
	push(&S, 5);
	push(&S, 6);
	traverse(&S); //遍历输出
	
	clear(&S);
	//traverse(&S); //遍历输出

	if ( pop(&S, &val) )
	{
		printf("出栈成功,出栈的元素是%d\n", val);
	}
	else
	{
		printf("出栈失败!\n");
	}

	traverse(&S); //遍历输出

	return 0;
}

void init(PSTACK pS)
{
	pS->pTop = (PNODE)malloc(sizeof(NODE));
	if (NULL == pS->pTop)
	{
		printf("动态内存分配失败!\n");
		exit(-1);
	}
	else
	{
		pS->pBottom = pS->pTop;
		pS->pTop->pNext = NULL; //pS->Bottom->pNext = NULL;
	}
}

void push(PSTACK pS, int val)
{
	PNODE pNew = (PNODE)malloc(sizeof(NODE));
	
	pNew->data = val;
	pNew->pNext = pS->pTop; //pS->Top不能改成pS->Bottom
	pS->pTop = pNew;

	return;
}

void traverse(PSTACK pS)
{
	PNODE p = pS->pTop;

	while (p != pS->pBottom)
	{
		printf("%d  ", p->data);
		p = p->pNext;
	}
	printf("\n");

	return;
}

bool empty(PSTACK pS)
{
	if (pS->pTop == pS->pBottom)
		return true;
	else
		return false;
}

//把pS所指向的栈出栈一次,并把出栈的元素存入pVal形参所指向的变量中,如果出栈失败,返回false,否则返回true
bool pop(PSTACK pS, int * pVal)
{
	if ( empty(pS) ) //pS本身存放的就是S的地址
	{
		return false;
	}
	else
	{
		PNODE r = pS->pTop;
		*pVal = r->data;
		pS->pTop = r->pNext;
		free(r);
		r = NULL;

		return true;
	}
}

//clear清空
void clear(PSTACK pS)
{
	if (empty(pS))
	{
		return;
	}
	else
	{
		PNODE p = pS->pTop;
		PNODE q = NULL;

		while (p != pS->pBottom)
		{
			q = p->pNext;
			free(p);
			p = q;
		}
		pS->pTop = pS->pBottom;
	}
}

应用

应用(算法):
1.函数调用

f(){
	g();
	return f;
}
g(){
	k();
	return g;
}
k(){
	return k;
}
//调函数为入栈,返回值为出栈???

2.中断
3.表达式求值
4.内存分配
5.缓存处理
6.迷宫

堆和栈的区别:
5个不同:
1.分配和回收机制不同——堆由程序员手动开辟与回收,栈由系统自动分配和回收;
2.存放内容不同——堆存你想存的数据,栈存放函数的相关参数,返回值,局变量,寄存器内容等;
3.地址增长方式不同——堆向地址增大方向增长,栈反过来;
4.大小不同——堆可开辟,理论课开辟整个虚拟内存,栈有上限;
5.效率——堆开辟慢,栈系统开辟快。

队列

定义

队列是先进先出。队列的情况和栈是一样的,也是容器适配器。
指定list 为起底层实现,初始化queue的语句如下:

std::queue<int, std::list<int>> third; // 定义以list为底层容器的队列

类型

1.静态队列(循环队列,内核为数组)
(1)减少内存浪费
(2)有两个指针front和rear,如果初始化,front和rear都为0;如果队列为空,front和rear相等;如果队列非空,front指向第一个元素,rear指向最后一个元素的下一个元素。
2.动态队列(链式队列,内核为链表)
3.优先队列(堆)

操作

1.用队列实现栈
用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。

以循环队列为列:
1.入队
rear = (rear + 1)%数组的长度
2.出队
front = (front + 1)%数组的长度
3.判断队列为空
front是否等于rear
4.判断队列为满
(1)增加标识参数
(2)少用一个元素

/*
  循环队列
*/
#include<stdio.h>
#include<malloc.h>
#include <stdbool.h>

typedef struct Queue{
  int * pBase;
  int front;
  int rear;
}QUEUE;

void init(QUEUE *);
bool en_queue(QUEUE *,int val);//入队
void traverse_queue(QUEUE *);
bool full_queue(QUEUE *);
bool out_queue(QUEUE *,int *pVal);//出队
bool emput_queue(QUEUE *pQ);


int main(void){
  QUEUE Q;
  int val;
  init(&Q);
  en_queue(&Q,1);
  en_queue(&Q,2);
  en_queue(&Q,3);
  en_queue(&Q,4);
  en_queue(&Q,5);
  en_queue(&Q,6);
  en_queue(&Q,7);
  en_queue(&Q,8);

  traverse_queue(&Q);
  if(out_queue(&Q, &val);){
    printf("出队成功,出队的元素是%d\n",val);
  }
  else{
    printf("出队失败!");
  }
  traverse_queue(&Q);
  return 0;
}
void init(QUEUE *pQ){
  pQ->pBase = (int*)malloc(sizeof(int) * 6);
  pQ->front = 0;
  pQ->rear = 0;

}
bool full_queue(QUEUE *pQ){
  if((pQ->rear + 1)%6 ==pQ->front)
    return true;
  else
    return false;
}

bool en_queue(QUEUE *pQ,int val){
  if(full_queue(pQ)){
    return false;
  }
  else{
    pQ->pBase[pQ->rear] = val;
    pQ->rear = (pQ->rear+1)%6;
    return true;
  }
}

void traverse_queue(QUEUE *pQ){
  int i = pQ->front;
  while(i != pQ->rear){
    printf("%d",pQ->pBase[i]);
    i = (i+1) % 6;
  }
  printf("\n");
  
  return;
}

bool emput_queue(QUEUE *pQ){
  if (pQ->front == pQ->rear){
    return true;
  }
  else{
    return false;
  }
}

bool out_queue(QUEUE *pQ,int *pVal){
  if(emput_queue(pQ)){
    return false;
  }
  else{
    *pVal = pQ->pBase[pQ->front];
    pQ->front = (pQ->front + 1) % 6;

    return true;
  }
}

/*
	链式队列
*/

# include <iostream>
using namespace std;

typedef struct node
{
	int data;
	struct node *pNext;
}NODE, *PNODE;

class Queue
{
public:
	Queue()
	{
		this->pHead = this->pTail = new NODE;
		this->pHead->pNext = NULL;
	}
	
	void InQueue(int val)
	{
		PNODE pNew = new NODE;

		pNew->data = val;
		pNew->pNext = NULL;
		
		pTail->pNext = pNew; //将pNew挂到队列尾部 
		pTail = pNew; //注意是尾指针上移
	
		return;
	}

	bool Empty() const
	{
		if (this->pHead == pTail)
			return true;
		else
			return false;
	}

	int OutQueue()
	{
		if (Empty())
		{
			cout <<"队列为空,无法出队!" << endl;
		}
		else
		{
			PNODE pTemp = pHead->pNext; //pHead不是要删除的队首元素,pHead->pNext所指向的元素才是要删除的元素,
			pHead->pNext = pTemp->pNext;
			int val = pTemp->data;

			delete pTemp;

			if (NULL == pHead->pNext) //如果队列为空
			{
				pTail = pHead; //尾指针也指向无用的头结点
			}
			
			return val;
		}
	}

	//遍历队列
	void Travers(void) const
	{
		PNODE pTemp = pHead->pNext;

		while (pTemp != NULL)
		{
			cout << pTemp->data << "  ";
			pTemp = pTemp->pNext;
		}
		cout << endl;
	}

	void Clear()
	{
		while (! this->Empty())
		{
			OutQueue();
		}
	}
	
	~Queue()
	{
		this->Clear(); 
		delete pHead;
	}

private:
	PNODE pHead, pTail; //pHead指向无用的头结点 pHead->pNext才是指向队首元素, pTail指向队尾元素
};

int main(void)
{
	Queue Q;

	for (int i=0; i<5; ++i)
		Q.InQueue(i+1);

	Q.Travers();

	Q.OutQueue();
	Q.OutQueue();

	Q.Travers();

	Q.Clear();
	Q.OutQueue();

	return 0;
}

应用

所有与时间有关的操作都与队列有关

定义

有且只有一个称为根的节点;有若干个互不相交的子树。

术语:
节点
父节点
子节点
子孙
堂兄弟

深度
叶子节点
非终端节点(非叶子节点)
度:子节点给个数
树的度:最大的度

类型

一般树

任意节点的子节点的个数不受限制

二叉树

(一定是有序树)
任意节点的子节点的个数最多两个,且子节点的位置不可更改。一般又可分为一般二叉树,满二叉树和完全二叉树

一般二叉树
满二叉树

除了最后一层的节点没有任何子节点外,每层上的所有节点都有两个节点的二叉树

完全二叉树

一颗二叉树的深度为h,除了第h层外,其他各层的节点都有两个子节点,且第h层的所有节点都集中在最左边
(满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树)

只删除了满二叉树最底层最右边的连续若干个节点

二叉搜索树(二叉查找树)

左子树的所有节点的值均小于它的根节点的值
右子树的所有节点的值均大于它的根节点的值
它的左右子树也分别为二叉搜索树

平衡二叉树

平衡树的定义:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1 。

class Solution {
public:
    // 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1
    int getHeight(TreeNode* node) {
        if (node == NULL) {
            return 0;
        }
        int leftHeight = getHeight(node->left);
        if (leftHeight == -1) return -1;
        int rightHeight = getHeight(node->right);
        if (rightHeight == -1) return -1;
        return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight);
    }
    bool isBalanced(TreeNode* root) {
        return getHeight(root) == -1 ? false : true;
    }
};
红黑树

又称R-B树,一种弱平衡的二叉搜索树
1.每个节点要么是黑色要么是红色
2.根节点是黑色
3.每个叶子节点是黑色,并且为空节点(还有另外一种说法就是,每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。)
4.如果一个节点是红色,则它的子节点必须是黑色
5.从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

确保从根节点道叶节点的最长路径的长度不超过最短路径的两倍。

堆是完全二叉树,所以一定是平衡二叉树。
分为大顶堆和小顶堆
在大顶堆中:父节点的值比每一个子节点的值都要大
在小顶堆中:父节点的值比每一个子节点的值都要小

存储

希望能够在线性的内存中保存非线性的数据结构
二叉树的存储:
连续存储(必须变成为完全二叉树,以数组为内核)->查找判断子节点的速度快,但耗内存
链式存储(以链表为内核,左右指针域)

一般树的存储:
双亲表示法
孩子表示法
双亲孩子表示法
二叉树表示法:把一个普通树转化为二叉树来存储(设法保证任意一个节点的左指针指向它的第一个孩子;右指针指向它的堂兄弟)

森林的存储:
二叉树表示法:方法类似于一般树到二叉树的转化

操作

1.深度优先遍历
(1)先序遍历(前序遍历)
访问根节点(中),再先序访问左子树(左),再先序访问右子树(右)

(2)中序遍历
中序访问左子树(左),再根节点(中),再中序访问右子树(右)

(3)后序遍历
后序访问左子树(左),再后序访问右子树(右),再根节点(中)

这里左中右,其实指的就是中间节点的遍历顺序:

如果知道两种遍历方式(除了先序和后序),就能推导出原二叉树:
已知先序和中序,求后序
已知中序和后序,求先序

# include <stdio.h>
# include <malloc.h>

struct BTNode
{
	char data;
	struct BTNode * pLchild; //p是指针 L是左  child是子节点
	struct BTNode * pRchild;
};

struct BTNode * CreateBTree(void);
void PreTraverseBTree(struct BTNode * pT);//先序
void InTraverseBTree(struct BTNode * pT);//中序
void PostTraverseBTree(struct BTNode * pT);//后序

int main(void)
{
	struct BTNode * pT = CreateBTree();
	
//	PreTraverseBTree(pT);
//	InTraverseBTree(pT);
	PostTraverseBTree(pT);
	
	return 0;
}

void PostTraverseBTree(struct BTNode * pT)
{
	if (NULL != pT)
	{
		if (NULL != pT->pLchild)
		{
			PostTraverseBTree(pT->pLchild);
		}	
		if (NULL != pT->pRchild)
		{
				PostTraverseBTree(pT->pRchild);
			//pT->pLchild可以代表整个左子树
		}
		printf("%c\n", pT->data);
	}
}

void InTraverseBTree(struct BTNode * pT)
{
	if (NULL != pT)
	{
		if (NULL != pT->pLchild)
		{
			InTraverseBTree(pT->pLchild);
		}
		
		printf("%c\n", pT->data);
	
		if (NULL != pT->pRchild)
		{
				InTraverseBTree(pT->pRchild);
			//pT->pLchild可以代表整个左子树
		}	
	}
}

void PreTraverseBTree(struct BTNode * pT)
{
	if (NULL != pT)
	{
		printf("%c\n", pT->data);
	
		if (NULL != pT->pLchild)
		{
			PreTraverseBTree(pT->pLchild);
		}
		
		if (NULL != pT->pRchild)
		{
				PreTraverseBTree(pT->pRchild);
			//pT->pLchild可以代表整个左子树
		}	
	}	

/*
	伪算法
	先访问根节点
	再先序访问左子树
	再先序访问右子树
*/
}

struct BTNode * CreateBTree(void)
{
	struct BTNode * pA = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode * pB = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode * pC = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode * pD = (struct BTNode *)malloc(sizeof(struct BTNode));
	struct BTNode * pE = (struct BTNode *)malloc(sizeof(struct BTNode));

	pA->data = 'A';
	pB->data = 'B';
	pC->data = 'C';
	pD->data = 'D';
	pE->data = 'E';

	pA->pLchild = pB;
	pA->pRchild = pC;
	pB->pLchild = pB->pRchild = NULL;
	pC->pLchild = pD;
	pC->pRchild = NULL;
	pD->pLchild = NULL;
	pD->pRchild = pE;
	pE->pLchild = pE->pRchild = NULL;

	return pA;
}

2.广度优先遍历
(1)层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。

需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑。

#迭代法
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> que;
        if (root != NULL) que.push(root);
        vector<vector<int>> result;
        while (!que.empty()) {
            int size = que.size();
            vector<int> vec;
            // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
            for (int i = 0; i < size; i++) {
                TreeNode* node = que.front();
                que.pop();
                vec.push_back(node->val);
                if (node->left) que.push(node->left);
                if (node->right) que.push(node->right);
            }
            result.push_back(vec);
        }
        return result;
    }
};
# 递归法(用深度优先遍历实现广度优先遍历)
class Solution {
public:
    void order(TreeNode* cur, vector<vector<int>>& result, int depth)
    {
        if (cur == nullptr) return;
        if (result.size() == depth) result.push_back(vector<int>());
        result[depth].push_back(cur->val);
        order(cur->left, result, depth + 1);
        order(cur->right, result, depth + 1);
    }
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> result;
        int depth = 0;
        order(root, result, depth);
        return result;
    }
};

3.翻转二叉树
image

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if(NULL == root) return root;
        //只能使用前序和后序,不能用中序
        invertTree(root->left);//左
        invertTree(root->right);//右
        swap(root->left,root->right);//中

        return root;
    }
};

4.对称二叉树
image
(1)排除空节点的情况
(2)排除数值不相同的情况
(3)递归,做下一层的判断

class Solution {
public:
    bool compare(TreeNode *left, TreeNode* right){
    //(1)排除空节点的情况
    if(left == NULL && right == NULL) return true;
    else if(left != NULL && right == NULL) return false;
    else if(left == NULL && right != NULL) return false;
    //(2)排除数值不相同的情况
    else if(left->val != right->val) return false;

    //(3)递归,做下一层的判断
    bool issame = compare(left->left, right->right) && compare(left->right,right->left);
    return issame;
}

    bool isSymmetric(TreeNode* root) {
        if(root == NULL) return root;
        return compare(root->left,root->right);
    }
};

5.最大和最小深度
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
(1)最大深度
根节点的高度就是最大深度。

//后序方式求最大深度
class solution {
public:
    int getdepth(TreeNode* node) {
        if (node == NULL) return 0;
        int leftdepth = getdepth(node->left);       // 左
        int rightdepth = getdepth(node->right);     // 右
        int depth = 1 + max(leftdepth, rightdepth); // 中
        return depth;
    }
    int maxDepth(TreeNode* root) {
        return getdepth(root);
    }
};

//进一步简化
class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        return max(maxDepth(root->left), maxDepth(root->right)) + 1;
    }
};


(2)最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。

求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。当一个左(右)子树为空,右(左)不为空,这时并不是最低点。

class Solution {
public:
    int getDepth(TreeNode* node) {
        if (node == NULL) return 0;
        int leftDepth = getDepth(node->left);           // 左
        int rightDepth = getDepth(node->right);         // 右
                                                        // 中
        // 当一个左子树为空,右不为空,这时并不是最低点
        if (node->left == NULL && node->right != NULL) { 
            return 1 + rightDepth;
        }   
        // 当一个右子树为空,左不为空,这时并不是最低点
        if (node->left != NULL && node->right == NULL) { 
            return 1 + leftDepth;
        }
        int result = 1 + min(leftDepth, rightDepth);
        return result;
    }

    int minDepth(TreeNode* root) {
        return getDepth(root);
    }
};


//进一步简化
class Solution {
public:
    int minDepth(TreeNode* root) {
        if (root == NULL) return 0;
        if (root->left == NULL && root->right != NULL) {
            return 1 + minDepth(root->right);
        }
        if (root->left != NULL && root->right == NULL) {
            return 1 + minDepth(root->left);
        }
        return 1 + min(minDepth(root->left), minDepth(root->right));
    }
};

应用

操作系统子父进程的关系本身就是一棵树
面向对象语言中类的继承关系本身就是一棵树
赫夫曼树

递归

1.明确递归函数的参数和返回值
2.明确终止条件
3.明确单层递归的逻辑(递归函数的功能)

例题:
1.阶乘
2.1到100的和
3.汉诺塔
4.走迷宫
5.如何自己直接或间接调自己?

//间接
f(){
  g();
}

g(){
  f()
}
//直接
f(){
  f();
}

递归必须满足三个条件:
1、递归必须有一个明确的中止条件
2、数据规模必须在递减
3、转化是可解的

循环和递归的关系:
递归易于理解,速度慢,存储空间大
循环不易理解,速度快,存储空间小

查找

二分查找

1.low high两个指针分别指向数组头和尾
2.mid = (low + high) / 2,并将nums[mid]与target比较
3.如果相等,返回;如果小于,则low移动;如果大于,则high移动
4.否则返回-1

int binary_search(int array[], int value, int size)
{
    int low = 0;
    int high = size -1;
    int mid;
    while(low <= high)
    {
        mid = (low + high) / 2;     // 二分
        if(array[mid] == value)     // 中间数据是目标数据
            return mid;
        else if(array[mid] < value) // 中间数据比目标数据小
            low = mid + 1;
        else   // 中间数据比目标数据大
            high = mid - 1;
    }
    return -1;
}
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int low = 0;
        int high = nums.size()-1;
        while(low <= high){
            int middle = (low + high) / 2; //int middle = left + ((right - left) / 2);//防止溢出

            if(nums[middle] < target){
                low =  middle + 1;
            }
            else if(nums[middle] > target){
                high = middle - 1;
            }
            else
                return middle;
        }
        return -1;
    }
};

排序

冒泡排序

插入排序

选择排序

快速排序

二分思维+递归思维

# include <stdio.h>

int FindPos(int * a, int low, int high);
void QuickSort(int * a, int low, int high);

int main(void)
{
	int a[6] = {-2, 1, 0, -985, 4, -93};
	int i;

	QuickSort(a, 0, 5); //第二个参数表示第一个元素的下标  第三个参数表示最后一个元素的下标
	
	for (i=0; i<6; ++i)
		printf("%d  ", a[i]);
	printf("\n");

	return 0;
}

void QuickSort(int * a, int low, int high)
{
	int pos;

	if (low < high)
	{
		pos = FindPos(a, low, high);
		QuickSort(a, low, pos-1);
		QuickSort(a, pos+1, high);
	}	
}

int FindPos(int * a, int low, int high)
{
	int val = a[low];

	while (low < high)
	{
		while (low<high  && a[high]>=val)
			--high;
		a[low] = a[high];

		while (low<high && a[low]<=val)
			++low;
		a[high] = a[low];
	}//终止while循环之后low和high一定是相等的

	a[low] = val; 

	return high; //high可以改为low, 但不能改为val 也不能改为a[low]  也不能改为a[high]
}

归并排序

堆排序

回溯算法

回溯法其实就是暴力查找,并不是什么高效的算法。它其实是一种采用递归思想的搜索算法。本质是穷举。

贪心算法

贪心的本质是选择每一阶段的局部最优,从而达到全局最优。类似于机器学习中集成学习算法。

动态规划

如果某一问题有很多重叠子问题,使用动态规划是最有效的。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

斐波那契数

#迭代法
class Solution {
public:
    int fib(int N) {
        if (N <= 1) return N;
        vector<int> dp(N + 1);
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= N; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[N];
    }
};

# 进一步优化,不用维护整个序列
class Solution {
public:
    int fib(int N) {
        if (N <= 1) return N;
        int dp[2];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= N; i++) {
            int sum = dp[0] + dp[1];
            dp[0] = dp[1];
            dp[1] = sum;
        }
        return dp[1];
    }
};
# 递归法
class Solution {
public:
    int fib(int N) {
        if (N < 2) return N;
        return fib(N - 1) + fib(N - 2);
    }
};
posted @ 2023-07-02 16:23  我好想睡觉啊  阅读(8)  评论(0编辑  收藏  举报