数据结构的学习

一、线性结构与非线性结构

线性结构:

特点是存在一对一的线性关系。

存在两种不不同的存储结构,顺序储存结构链式存储结构

顺序储存的线性表叫做顺序表,它的存储元素是连续的。

链式储存的线性表叫做链表,它的存储元素不一定是连续的。

常见:数组、队列、链表和栈。

非线性结构:二维数组、多维数组、广义表、树结构、图结构。

二、稀疏数组

先看实际需求:

在编写五子棋程序当中,用0来表示棋盘,但是会存在大量的0,记录了很多无意义的数据,这时候就用到稀疏数组。

基本介绍:

当一个数组当中大部分为0,或者为同一个值的数组时,可以使用稀疏数组来保存。

处理方法:

记录数组有几行几列,有多少个不同值

将不同值元素的行列以及它的值记录在一个小规模的数组中,从而缩小规模。

应用场景:

二维数组 转 稀疏数组的思路:

1.遍历 原始的二维数组,得到有效数据的个数sum

2.根据sum就可以创建稀疏数组sparseArr int[ sum+1 ] [ 3 ]

3.将二维数组的有效数据存入 到 稀疏数组

稀疏数组 转 二维数组的思路:

1.先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,

比如:charArr = int[11][11]

2.在读取稀疏数组后几行的数据,并赋给原始的二维数组。

代码实现:

课后练习:

1.在前面的基础上,将稀疏数组保存到磁盘上,如:map.data

2.恢复原来的数组,读取map.data进行恢复。

三、队列(先进先出)

队列是一个有序列表,可以用数组(顺序储存)或是链表(链式存储)来实现。

数组模拟队列:maxSize表示最大容量。

用到两个变量front,rear。分别表示队列的前后端。

front会随着数据输出而改变。rear会随着数据输入而改变。

思路分析:

将数据存入队列称为“addQueue”

将尾指针往后移,rear+1

当front==rear 【队列空】,rear == maxSize - 1[ 队列满 ]

若尾指针rear小于队列的最大小标maxSize-1,将数据存入rear所指的数组元素中,否则无法存入数据。

将数据取出队列称为“getQueue”

将头指针往后移:front+1。

注意:初始rear和front都是-1.因为一开始没有数据存入,它们的值都是头部的前一个指针

代码实现:

package da;

import java.util.Arrays;
import java.util.Scanner;

public class QueueTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Queue queue = new Queue(3);
//		queue.addQueue(1);//测试
//		queue.addQueue(2);
//		queue.getQueue();
//		queue.getQueue();
//		queue.showQueue();
//		queue.showHeader();
		
		//编写算法和界面展示
		Scanner scanner = new Scanner(System.in);
		char key;
		boolean loop = true;
		while(loop){
			System.out.println("****************");
			System.out.println("s(show): 显示队列");
			System.out.println("a(add): 添加数据");
			System.out.println("g(get): 取出数据");
			System.out.println("h(head): 头部数据");
			System.out.println("按其他任意键退出...");
			
			//没见识过这个用法,记下来
			key = scanner.next().charAt(0); 
			switch (key) {
			case 'a':
				System.out.println("请添加一个数据:");
				queue.addQueue(scanner.nextInt());
				
				break;
			case 's':
				System.out.println("显示队列:");
				queue.showQueue();
				break;
			case 'g':
				//try方法,用的比较少,建议再去复习看看,这个用起来感觉挺流畅的。
				try {
					System.out.println("取出的数据:"+queue.getQueue());
				} catch (Exception e) {
					// TODO: handle exception
					System.out.println(e.getMessage());
				}
				
				
				break;
			case 'h':
				System.out.println("显示头部数据:");
				queue.showHeader();
				break;

				//没用过,记下来
			default:
				scanner.close();
				System.out.println("程序退出");
				loop = false;
				break;
			}
		}
		

	}

}

class Queue{
	int maxSize;
	int front=-1,rear=-1;
	//设置队列空间
	int queue[];
	
	//默认构造方法,定义队列最大空间
	public Queue(int max){
		this.maxSize=max;
		queue = new int[max];
	}
	
	//入队
	void addQueue(int n){
//		queue = new int[maxSize];  //这里出错,因为每次都调用了一次它。
		if(rear == maxSize -1){
			System.out.println("队列已满,不能添加元素");
			
			//原来还可以这样用,没见过,记下来!!!
			return;
		}
		rear = rear+1;
		queue[rear]= n;
	}
	
	//出队
	int getQueue(){
		if(rear == front){
//			System.out.println("队列为空");
//			return;
			throw new RuntimeException("队列为空不能取出数据!!");
		}
		front=front+1;
		int item = queue[front];
		queue[front] = 0;
		return item;
	}
	
	//展示队列
	void showQueue(){
//		return Arrays.toString(queue);
		
//		if(rear == maxSize -1){
//			System.out.println("队列已满");
//			return;
//		}
		for (int i = 0; i < queue.length; i++) {
			System.out.printf("queue[%d]=%d\n",i,queue[i]);
		}
	}
	
	//展示队列头
	void showHeader(){
		if(rear == front){
			System.out.println("队列为空,不能取出元素");
			return;
		}
		System.out.println("head:"+queue[front+1]);
		
	}
	
}

问题分析并优化:

1.目前数组使用一次就不能使用,没有到达复用的效果

2.将这个数组使用算法,改成一个环形的队列。(取模%)

数组模拟分析环形队列:(通过取模的方式实现)

尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定。

思路如下:

1.front变量的含义调整:front指向队列的第一个元素。即front的初始值为0

2.rear变量的含义调整:rear指向队列的最后一个元素的后一个位置,希望空出一个空间做约定。rear的初始值为0

3.当队列满时,(rear+1)%maxSize == front 【满】  //这样理解,因为初始值为0,rear指向最后一个元素时索引是maxSize-1。所以为满的情况是(rear+1)

4.队列空,rear= = front 【空】

5.队列中有效个数:(rear+maxSize - front)%maxSize //

6.最后得到一个环形队列。

代码实现:

package da;

import java.util.Scanner;

public class CicrleQueueTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub

		//说明设置4,其队列有效数据最大是3.!!!这个千万注意,之前就是设置为3,其有效数据为2.结果一直没搞明白为什么只有两个数据。
		CicrleQueue queue = new CicrleQueue(4);
		
		//编写算法和界面展示
		Scanner scanner = new Scanner(System.in);
		char key;
		boolean loop = true;
		while(loop){
			System.out.println("****************");
			System.out.println("s(show): 显示队列");
			System.out.println("a(add): 添加数据");
			System.out.println("g(get): 取出数据");
			System.out.println("h(head): 头部数据");
			System.out.println("按其他任意键退出...");
			
			//没见识过这个用法,记下来
			key = scanner.next().charAt(0); 
			switch (key) {
			case 'a':
				System.out.println("请添加一个数据:");
				queue.addQueue(scanner.nextInt());
				
				break;
			case 's':
				System.out.println("显示队列:");
				queue.showQueue();
				break;
			case 'g':
				try {
					System.out.println("取出的数据:"+queue.getQueue());
				} catch (Exception e) {
					// TODO: handle exception
					System.out.println(e.getMessage());
				}		
				break;
			case 'h':
				System.out.println("显示头部数据:");
				queue.showHeader();
				break;

				//没用过,记下来
			default:
				scanner.close();
				System.out.println("程序退出");
				loop = false;
				break;
			}
		}
	}

}

class CicrleQueue{
	int maxSize;
	//默认情况下是0
	int front=0,rear=0;
	//设置队列空间
	int queue[];
	
	//定义构造器
	public CicrleQueue(int max){
		this.maxSize=max;
		queue = new int[max];
	}
	
	//入队
	void addQueue(int n){
		if((rear+1)%maxSize == front){
			System.out.println("队列已满,不能添加元素");
			return;
		}
		queue[rear]= n;
		rear=(rear+1)%maxSize;
	}
	
	//出队
	int getQueue(){
		if(rear == front){
			throw new RuntimeException("队列为空不能取出数据!!");
		}
//		front=front+1;
		int item = queue[front];
		front=(front+1)%maxSize;
		return item;
	}
	
	//展示队列
	void showQueue(){
		if(rear == front){
			System.out.println("队列为空");
			return;
		}
		for (int i = front; i < front+(rear-front+maxSize)%maxSize; i++) {
			System.out.printf("queue[%d]=%d\n",i,queue[i]);
		}
	}
	
	//展示队列头
	void showHeader(){
		if(rear == front){
			System.out.println("队列为空,不能取出元素");
			return;
		}
		System.out.println("head:"+queue[front]);
		
	}
}

四、单链表(带头节点)

1.链表以节点方式存储。是链式存储结构。

2.每个节点包含data域,next域:指向下一个节点

3.节点不一定连续存储

4.链表分带头节点的链表和没有头结点的链表。根据实际情况需要

应用举例:

使用带head头的单向链表实现--水浒英雄排行榜管理。

要求:完成对英雄的增删改查操作。

有两种方式,第一种方式:在添加英雄时,直接添加到链表的尾部。

第二种方式:在添加英雄时,根据排名将英雄插入到指定位置。

先介绍第一种方式:

思路分析:

1.先创建一个head头结点,作用是表示单链表的头。不存放具体的数据。

2.后面我们每添加一个节点,就直接加入到 链表的最后。

遍历:

1.通过一个辅助变量遍历,帮助遍历整个链表。

画UML图:(这个很重要,学会这个好写代码)

HeroNode类表示英雄属性。

SingleLinkedList类表示管理英雄。

代码实现:

一些代码的作用介绍:

next变量的作用,记住吧!它的作用就是用来指向下一个节点。在本题中,它就是用来指向下一个英雄,所以,它可以为:temp.next == null。

在HeroNode使用toSting方法的原因:

`toString()` 是一个重写自 `Object` 类的方法。它的作用是返回一个对象的字符串表示形式。当我们打印一个对象时,例如使用 `System.out.println(object)`,实际上调用的就是这个对象的 `toString()` 方法。

在这个例子中,`HeroNode` 类重写了 `toString()` 方法,返回了一个包含节点编号、名称和昵称的字符串。因此,当我们打印一个 `HeroNode` 对象时,会输出这个对象的编号、名称和昵称。

学到一个快捷键:

在MyEclipse中,可以使用快捷键`Ctrl+Alt+Down`复制当前行到下一行,使用快捷键`Ctrl+Alt+Up`复制当前行到上一行。
 

第二种方式:按编号顺序添加的链表(链表的顺序插入)

问题描述:第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)

思路分析:

1.首先找到新添加的节点的位置

2.新的节点.next = temp.next

3.将temp.next = 新的节点

UML图:addByOrder(HeroNode):void   添加一个flag表示排名。

算法分析:就是学会注释吧。

代码分析:

单链表节点的修改:

思路分析:修改(英雄)节点信息,根据编号no修改,编号本身不能修改。

自己写的:1.遍历链表,找到需要修改的节点。用flag表示是否找到

2.找到需要修改的节点,用新节点进行替换。

UML:update(HeroNode):void       boolean flag = true;

算法分析:

代码分析:

void update(HeroNode newhero){
		//用flag表示是否找到节点
		boolean flag=false;
		HeroNode temp = head.next;
		
		//1.遍历链表,找到需要修改的节点。
		while(true){
			if(temp == null){
				break;
			}
			//当需要修改的节点和遍历找到的编号相同,找到需要修改的节点
			if(temp.no==newhero.no){
				flag = true;
				break;
			}
			temp=temp.next;
		}
		//2.将找到的需要修改的节点,用新节点进行替换。
		//误区:以为直接替换就可以了(不能直接将一个对象赋给一个对象),条件是编号不动,所以替换name和nickname就可以
		if(flag){
			temp.name=newhero.name;
			temp.nickname=newhero.nickname;
		}else{
			System.out.printf("没有找到编号%d的节点,不能修改\n",newhero.no);
		}
	}

误点:思路分析第二点,以为可以直接将新节点的对象替换过去。对象是不能赋予的。

单链表节点的删除:

思路分析:

1.遍历链表,找到需要删除这个节点的前一个节点temp

2.temp.next = temp.next.next

3.被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收

UML:del(int):void     flag判断是否找到待删除节点

代码分析:自己敲吧

练习:添加单链表的查询功能。

五、单链表面试题

(1).求单链表中有效节点的个数

自己编码

(2).查找单链表中倒数第K个结点   【新浪面试题】

思路分析:

1.编写一个方法,接收head节点,同时接受一个index

2.index表示是倒数第index个节点

3.遍历整个链表,得到链表总长size

4.从链表第一个遍历到(size- index)个。就能获取到

5.如果找到了就返回该节点,否则返回Null.

UML图:findLastIndexNode(HeroNode,int):HeroNode

误区点:

1.//判断是否是所需要的节点,
        //误区:一直以为判断是否所需节点,从temp身上判断,其实也可以从index判断,
        //index只要满足在一定条件,那么最后遍历得到必定是所需要的节点。

2.findLastIndexNode是一个HeroNode类型,那么它的返回类型必定是一个HeroNode类型。

即最后必须加上:return temp;    //这样才是一个完整的方法。

扩展:void类型不能返回实体,只能返回return;   //这样才可以。

而其它类型的返回,就按自己的需求定义来写。(这个方面多加练习。)

3.在主类写实现的那个方法,怎么将一个head节点的参数传入进去?因为head节点的参数在SingleLinkedList类里面。那么就通过这个参数里面书写:getHead方法,return head 。这样在主类中通过它的方法就能得到传入参数head节点。!!!

HeroNode res=heroNode.findLastIndexNode(heroNode.getHead(), n);

getHead属于对象heroNode的方法,得到head节点。findLastIndexNode是一个HeroNode类型(字符串),所以需要用res来保存。最后用sysout输出。

(3)单链表的反转 【腾讯面试题】

思路分析:

1.先定义一个节点reverseHead = new HeroNode();

2.从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端。!!!(很重要,这一步决定了最后一次遍历的节点会放在第一个位置)

3.原来的链表的head.next = reverseHead.next  (完成替换,即反转结束。)

我的看法:这是一个很有意思的反转。从普通的角度去看,

要实现反转,那么必须含有两个及以上的的节点。那么每一个节点我们都用一个辅助变量去表示。

cur表示原先链表头结点的下一个节点,next先暂时为空,它用来保存cur的下一个节点。(这样至少是两个节点了)

开始指向:

1.首先next指向cur的下一个节点。next=cur.next(保存cur的下一个节点)

2.cur.next指向反转链表的下一个节点。cur.next=reverseHead.next(!!很重要,它指向是null。为下一个cur的指向做准备)

3.reverseHead.next指向原先链表的下一个节点。reverseHead.next=cur (这里开始,反转链表才开始指向原先链表取出的节点。而cur指向的就是上一个节点的null。这样就构成了基本的链式结构。)

4.cur指向第二个节点。cur=next 。cur往后移。

细心一点发现,其实从第二步到第三步就是一个链式结构。从普通的角度出发,我们写这个程序都是直接先写了第三步,然后发现怎么走都走不通。就是因为在写第三步的时候,一定要先确认它一开始会指向谁。

先记住吧:基本链式结构,下一个的指向要为上一个指向能构成链式结构。(以后这样想,这样去写链式结构就不会出错。)

(错了,不能,如果将第一步放到一开始的定义,会导致next始终为一个固定值)尝试思考训练:重新思考上面的结构就能发现,其实第一步可以放到一开始的定义里面,从而省略掉第一步。真正的顺序只有三步,而不是四步。

代码分析:

//实现单链表的反转 
	//思路分析,1.首先定义一个反转头结点 reverseHead =new HeroNode();
	//2.从头到尾遍历原先的单链表,获得每一个节点,并取出来放在反转链表的最前端。(也就是反转头结点的后一个位置)
	//3.head.next = reversehead.next
	
	//因为要得到反转链表,这里就写HeroNode类型的方法
	public void reverseLinkedList(HeroNode head){
		//定义反转头节点
		HeroNode reverseHead = new HeroNode(0, "", "");

		HeroNode temp = head.next;
		//首先判断链表实现的条件,至少存在两个节点
		if(temp==null || temp.next==null){
			System.out.println("链表为空");
		}
		//遍历链表
		HeroNode next = null;
		while(temp!=null){
			//构建新的反转链式结构
			//误区:以为这个可以放到前面去,不能,因为这是一个遍历。如果放前面去next的指向会保持不变。
			next = temp.next;   
			temp.next = reverseHead.next;
			reverseHead.next = temp;
			temp = next;
		}
		//循环结束
		head.next=reverseHead.next;
	}
		
//		while(true){
//			if(temp.next==null){
//				break;
//			}
//			//每遍历一个节点就要放在链表最前端
//			reverseHead.next=temp.next;
//			//需要保存取出的节点,因为在遍历的时候,需要让放在最前端的节点指向它。
//			//误区:逻辑出现混乱
//			HeroNode cur = reverseHead.next;
//			temp.next = cur;
//			temp=temp.next;
//			
//		}

(4)从尾到头打印单链表。【百度面试题】

思路分析:

1.不建议使用上面的发转,题目的要求是逆序打印单链表,反转会破坏单链表结构。

2.利用栈的先进后出的特点,可以实现逆序打印的效果。

思路:1.首先创建一个栈,将链表数据压入栈中。

           2.在通过遍历,将栈弹出。就能得到一个逆序打印的单链表。

在讲栈之前,先回顾一下泛型:泛型就是类后面加了一个<>。这个里面可以是各种类型。这样能得到一个此类型的数据而不用再去进行转换。

栈:其实就是一个特别的泛型结构。用Stack<E>表示栈类。

创建一个栈:Stack<Stirng> stack = new Stack<Stirng>();

那么对象stack就是一个String类型的集合。

它存在两个方法:对象.push(E item)   。上面那个E是String 。所以这个里面就是:“字符串”

对象.pop()。

代码分析:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值