1.线性表
线性表(liner list):是n个具有相同特性的数据元素的有限序列.常见的线性表有:顺序表,链表,栈,队列和字符串等.线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储.
2. 顺序表
2.1 顺序表定义
顺序表是用一段物理地址连续的存储单元一次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删改查.
2.2静态与动态
顺序表又可以分为静态顺序表和动态顺序表.静态顺序表使用定长数组存储,当我们确定知道需要存储多少数据的时候,静态顺序表就比较适用.但是静态顺序表的定长数组导致N定大了,会浪费空间,定少了,空间不够用,相比之下动态顺序表会更加灵活,可以根据需要动态分配空间大小
2.3顺序表实现
顺序表本质上就是一个数组,那么我们为什么还要写一个数组表呢?直接用数组不就好了吗?
是因为,我们将操作数组的方法放到类里面,将来就可以面向对象了!
于是我们创建一个类叫做顺序表:
public class MyArrayList {
public int[] elem;
public int usedSize;//表示当前有效的数据个数
public MyArrayList(){
this.elem = new int[10];
}
//1.打印顺序表
public void display(){
for (int i = 0; i < usedSize; i++) {
System.out.print(this.elem[i]);
}
System.out.println();
}
//2.获取顺序表的有效数据长度
public int size(){
return this.usedSize;
}
//3.在pos位置新增元素
//pos < 0 || pos > usedSize 此时插入的位置不合法
//usedSize == len 此时需要扩容
public void add(int pos ,int data){
if(pos < 0 || pos > usedSize){
System.out.println(pos +" 位置不合法!");
}
//如果满了就需要扩容
if(isFull()){
//扩容2倍
this.elem = Arrays.copyOf(this.elem,elem.length*2);
}
for (int i = this.usedSize - 1; i >= pos; i--) {
this.elem[i+1] = this.elem[i];
}
this.elem[pos] = data;
this.usedSize++;
}
//判断数组是否满了
public boolean isFull(){
return (this.usedSize == elem.length);
}
//4.判断顺序表内是否包含某个元素,包含返回true,不包含返回false
public boolean contains(int toFind){
if(usedSize == 0)return false;
for (int i = 0; i < usedSize; i++) {
if(this.elem[i] == toFind){
return true;
}
}
return false;
}
//5.查找某个元素对应的位置,找不到返回-1
public int search(int toFind){
for (int i = 0; i < this.usedSize; i++) {
if(this.elem[i] == toFind){
return i;
}
}
return -1;
}
//6.获取pos位置的元素
public int getPos(int pos){
if(pos < 0 || pos >= usedSize){
System.out.println(pos +" 位置不合法!");
return -1;//这里返回-1是业务上的处理,这里暂不考虑,后期可以抛出异常
}
if(isEmpty()){
System.out.println("顺序表为空!");
return -1;
}
return elem[pos];
}
//判断是否为空
public boolean isEmpty(){
return this.usedSize == 0;
}
//7.给pos位置的元素设置为value
public void setPos(int pos,int value){
if(pos < 0 || pos >= this.usedSize){
System.out.println(pos +" 位置不合法!");
return;
}
if(isEmpty()){
System.out.println("顺序表为空!");
return;
}
this.elem[pos] = value;
}
//8.删除第一次出现的关键字 toRemove
public void remove(int toRemove){
if(isEmpty()){
System.out.println("顺序表为空!");
return;
}
int index = search(toRemove);//这里调用search方法,返回toRemove元素所在的下标
if(index == -1){
System.out.println("没有我们要删除的数字!");
return;
}
for (int i = index; i < usedSize -1; i++) {
this.elem[i] = this.elem[i+1];
}
this.usedSize--;//这里成功删除,元素个数减一
return;
}
//9.清空顺序表
public void clear(){
this.usedSize = 0;
//如果顺序表里面存放的是引用类型,就必须遍历顺序表将所有的元素置为空,才算真正清空顺序表
// for (int i = 0; i < usedSize; i++) {
// this.elem[i] = null;
// }
}
}
3.单链表
3.1链表定义
链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的引用次序来实现的.根据是否带头,单向还是双向,以及是否循环,链表共有 2 x 2 x 2 = 8种.
我们主要学习不带头非循环的单链表和不带头非循环的双向链表这两种
3.2单链表的实现
写好单链表的题目,前提是要对单链表的结构了然于胸,一个属性是val值,另一个属性是引用,指向下一个节点.特别是后一个属性,一定要安排的明明白白,不然就会出现异常
//ListNode这个类代表一个节点
class ListNode{
public int val;
public ListNode next;
public ListNode(int val){
this.val = val;
}//可以通过这个构造器创建单链表
}
//MyLinkedList这个类是链表
public class MyLinkedList {
public ListNode head;
//链表的头引用,这个head以后永远指向链表的头部分,head是链表的属性,而不是节点的属性,不能混淆
//1.穷举法创建单链表
public void createList(){
ListNode listNode1 = new ListNode(12);
ListNode listNode2 = new ListNode(23);
ListNode listNode3 = new ListNode(34);
ListNode listNode4 = new ListNode(45);
ListNode listNode5 = new ListNode(56);
this.head = listNode1;
listNode1.next = listNode2;
listNode2.next = listNode3;
listNode3.next = listNode4;
listNode4.next = listNode5;//这里listnode5.next默认是null
}
//2.单链表的遍历
public void display(){
ListNode cur = this.head;//cur为head的替身
//head这个属性是属于单链表的不能随意改动!!!
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//3.查找关键字key是否在单链表中
public boolean contains(int key){
ListNode cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;//这里就无须担心单链表是否为空这个问题了,如果为空就不会进入while循环,最后也是返回false
}
//4.得到单链表的长度
public int size(){
int count = 0;
ListNode cur = head;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
//5.头插法插入一个节点
public void addFirst(int data){
ListNode node = new ListNode(data);//先要创建需要插入的节点
node.next = head;//这里就算是空的链表也可以做到,head == null就是了
head = node;
}
//6.尾插法插入一个节点
public void addLast(int data){
//尾插法第一次必须要先判断head是否为空,如果为空会造成空指针异常
ListNode node = new ListNode(data);
if(head == null){
head = node;
}else{
ListNode cur = this.head;
//一定要注意是cur.next != null;
while(cur.next != null){
cur = cur.next;
}
//出了循环后cur.next == null;就代表cur是最后一个节点
cur.next = node;
}
}
//7.在链表的任意一个位置插入一个节点.第一个数据节点的下标为0
//由于单链表只有一个尾结点,因此如果我们要在某个位置插入一个节点,就必须找到它的前驱
//我们可以先写一个方法,通过你这个方法来返回需要插入节点前驱的 引用 !!!
public ListNode findIndex(int index){
ListNode cur = head;
for (int i = 0; i < index-1; i++) {
cur = cur.next;
}
return cur;
//1.如果插入位置为0,返回结果就是head,显然head不可能作为前驱.因此在插入的时候要单独的判断,我们可以直接头插
//2.链表的插入位置还有可能是size(),此时我们得到的是最后一个元素的引用,我们可以直接尾插
}
public void addIndex(int index,int data){
if(index < 0 || index > size()){
System.out.println("插入位置不合法!");
return;
}
if(index == 0){
addFirst(data);//此时找不到节点的前驱,直接用头插法
return;
}
if(index == size()){
addLast(data);//要插入节点的位置在链表末尾,可以直接尾插法
return;
}
ListNode node = new ListNode(data);
ListNode cur = findIndex(index);
node.next = cur.next;
cur.next = node;
}
//8.删除第一次出现关键字key的节点
//删除某个关键字和上面插入某个关键字有相似的地方,那就是我们都要先找到被操作节点的前驱
//在这里我们写一个方法,得到这个前驱的引用
public ListNode findPrev(int key){
ListNode cur = head;
//返回前驱提前是有前驱,当链表为空,或者第一个元素就是我们要找的key,
//就没有前驱可以返回,这种情况要在删除方法里面单独处理,之后才能调用我们的findPrev方法
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
//出了while循环后,就代表我们没有碰到值为key的节点
return null;
}
public void remove(int key){
if(head == null){
System.out.println("单链表为空! 不能删除!");
return;
}
if(this.head.val == key){
this.head = this.head.next;
}
ListNode cur = findPrev(key);
if(cur == null){
System.out.println(key +" 不在链表当中!");
return;
}
cur.next = cur.next.next;
}
//9.遍历单链表一遍,删除所有关键字为key的节点
//这里要求遍历单链表一遍,因此我们就不能通过循环删除单个值为key的方法来实现,那我们就一个一个的来吧
public void removeAllKey(int key) {
if (this.head == null) return;
ListNode prev = this.head;
ListNode cur = this.head.next;
while (cur != null) {
if (cur.val == key) {
prev.next = cur.next;
cur = cur.next;
} else {
prev = cur;
cur = cur.next;
}
if (this.head.val == key) {
this.head = this.head.next;
}
return;
}
}
//10.清空单链表
public void clear(){
//this.head = null;//粗暴的方法
while(this.head != null){
ListNode curNext = head.next;
this.head.next = null;
this.head = curNext;
}
}
//11.反转一个单链表
//解题思路:我们可以将这个问题想象成拧螺丝,我们需要将螺丝钉上面的一串螺丝一个个拧下来,然后挨个穿到另一个螺丝钉上.
//假设我们的链表为空,我们无需反转,返回一个null即可
//我们定义一个节点cur代表我们将要移动的节点,首先我得知道将它指向谁,于是我们定义一个prev代表在它之前移动的节点
//然后cur.next = prev;即可
//之后cur这个节点就变成前驱了,于是prev = cur;
//然而cur移完之后下一个移动谁呢?于是,我们在cur移动之前,需要知道下一个节点在哪里.
// 于是,我们创建一个节点curNext = cur.next;之后cur = curNext;即可
//当我们移动到cur == null的时候就代表我们移动完成了,此时prev就代表我们的最后一个节点,返回prev即可!
public ListNode reverseList(){
if(head == null){
return null;
}
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode curNext = cur.next;
cur.next = prev;
prev = cur;
cur = curNext;
}
//能够到这里表示cur == null,我们直接返回prev即可,它就是我们反转之后的头节点
return prev;
}
//12.返回中间节点:给定一个带有头结点head的非空单链表,返回链表的中间节点,如果有两个中间节点,返回后一个
//要求:遍历单链表一遍
//解题思路:快慢指针,fast一次走两步,slow一次走一步.速度是二倍那么路程也是二倍,一个到结尾了,另一个就在中间位置
//然而问题是,这个过程是离散的,我们还需要考虑一些细节问题
//结束条件:解决问题的关键就是,当循环结束的时候,slow正好在中间位置(偶数时是后一个)
//假设节点个数为奇数,当fast走到最后一个节点的时候,也就是fast.next == null的时候,slow正好走到中间位置
//当节点个数为偶数的时候,当fast走到最后一个节点的后一个节点的时候,也就是fast = null时,slow走到中间的位置
//我们可以将这两种情况综合起来,fast == null 和fast.next == null,两种情况是不可能同时发生的
//奇数的情况下肯定是fast.next == null,偶数一定是fast == null;
//于是我们就没有必要分开讨论直接while(fast.next != null && fast != null)即可,满足其一即不符合结束条件
public ListNode middleNode() {
if (head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while (fast.next != null && fast != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
//13.输入一个链表找到倒数第k个节点
//要求:遍历单链表一遍完成
//解题思路1:假设一个班有n个学生,将他们按照成绩划分,
//那么倒数第1名就是正数第n名,倒数第2就是正数第n-1,倒数第3就是正数第n-2,倒数第k就是正数第n-k-1;
//假设这个题目没有要求遍历单链表一遍,那么我们直接遍历数组得到节点个数,然后找到第n-k-1个节点即可,这种方法比较简单可以自己尝试一下
//解题思路2:假设班级里面总共就有k个学生,假设你是第1名,那么同时你也是倒数第k名.
//然而这个班的人数还没有满,学校要将尖子班的小孩调到你班,这些小孩就算是成绩最差的也比你好
//在这个过程中你和你们班的同学名次整体往下掉,直到班级人满为止
//在这个过程中,你永远都是倒数第k名
//根据这个思想我们的代码就可以这样写:
public ListNode findLastK(int k) {
if (k <= 0 || head == null) { //k如果是负数,或者链表为空就找不到倒数第k个节点
return null;
}
ListNode last = head;
ListNode cur = head;
for (int i = 0; i < k - 1; i++) {
last = last.next;
if (last == null) {
return null;
}//这个判断一定不能忘记,否则会出现空指针异常
}//寻找当前班里的倒数第一名
while (last.next != null) {
last = last.next;
cur = cur.next;
}
return cur;
}
//13.将两个有序的单链表合并成一个新的有序的单链表并返回,新链表是通过拼接给定的两个单链表的所有节点组成的
//解题思路:需要对两个有序链表的节点值进行比较,将小的值插入到链表的后面即可,
//当其中一个链表遍历完成剩下的直接插入即可
public ListNode mergeTowLists(ListNode headA, ListNode headB) {
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
while (headA != null && headB != null) {
if (headA.val < headB.val) {
tmp.next = headA;
tmp = tmp.next;
headA = headA.next;
} else {
tmp.next = headB;
tmp = tmp.next;
headB = headB.next;
}
}
//while循环结束后,有一个链表已经遍历完毕
if (headA == null) {
tmp.next = headB;
} else {
tmp.next = headA;
}
return newHead.next;
}
//14.现有一链表的头指针ListNode head ,给一定值x,编写一段代码将所有小于节点x的节点排在其余节点之前,
//且不改变原来数据的顺序,返回重新排列后的链表头指针
//解题思路:我们可以直接遍历单链表.在遍历的过程中我们根据节点的值,将节点分成两个单链表,然后再将两个单链表合并即可
public ListNode partition(int x) {
ListNode bs = null;//前面部分的头节点
ListNode be = null;//前面部分的尾结点
ListNode as = null;//后面部分的头节点
ListNode ae = null;//后面部分的尾结点
ListNode cur = head;
while (cur != null) {
if (cur.val < x) {
if (bs == null) { //前面部分,第一次插入的时候
bs = cur;
be = cur;
} else { //前面部分不是第一次次插入
be.next = cur;
be = be.next;
}
} else {
if (as == null) {//后面部分第一次插入
as = cur;
ae = cur;
} else { //后面部分不是第一次插入
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
//走出while循环后cur == null,节点分配完毕
//之后我们要做的就是将两个链表链接起来
if (bs == null) {//前面部分的尾结点还是空的,意味着返回as即可
return as;
}
//如果不判断 直接bs.next = as; 会造成空指针异常
be.next = as;//将两个链表链接起来
//预防后面部分,最后一个节点的next不是空的否则将会成环
if (as != null) {
ae.next = null;
}
return bs;
}
//15.在一个排序了的链表中,存在重复的节点,请删除该链表中重复的节点,重复的节点不保留,返回链表头指针
//解题思路:由于链表内的数据是排序了的,因此.我们可以通过将链表分段,每一段的元素值都是相同的
//通过遍历,我们将有重复数据的段跳过,然后将其余的节点依次链接,组成一个新的单链表即可
public ListNode deleteDuplication() {
ListNode cur = head;
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
while (cur != null) {
if (cur.next != null && cur.val == cur.next.val) {//这里要注意顺序,不然会出现空指针异常
//进入循环条件,说明,cur 和 cur.next 位置的元素是相同的,而我们tmp下一个要链接的节点,在下一个段
///因此我们首先要找到这个段里的最后一个元素,于是有了下面的while循环,将cur后移
while (cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
cur = cur.next;//这次cur就指向下一段的第一个节点
} else { //cur.next.val 与 cur.val不相同,或者cur.next==null,cur是最后一个节点
tmp.next = cur;
tmp = tmp.next;
cur = cur.next;
}
}
//如果单链表最后的几个元素是空的,经过while循环,cur指向链表的最后一个元素,之后cur = cur.next 变为null
//由于cur变为了空,最外层的while循环便再也进不去,而此时tmp指向的是前面的那个元素,tmp.next还没有指定,
//因此我们需要手动进行指定,tmp.next = null,才可以,否则,最后几个重复的元素就不会被删除!!!
tmp.next = null;
return newHead.next;
}
//16.给定一个单链表,判断这个单链表是否有环
//解题思路:如果一个链表有环,那么这个环一定不可能出现在中间位置,因为一个引用不可能指向两个节点
// 因此环一定出现在末尾,那么如果我遍历链表的话,一定会在尾巴的环中进入死循环状态
//我们可以采用快慢指针,fast一次走两步,slow一次走一步,而他们终究会在环里面相遇
//如果相遇返回true,如果fast.next == null 或者 fast == null,即代表单链表中无环返回false
public boolean hasCycle(){
if(head == null) return false;//空链表不存在循环
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){//顺序不能变,否则会空指针异常
fast = fast.next.next;
slow = slow.next;
if(slow == fast){
return true;
//这里也可以直接break,通过fast是否为空来进行判断
}
}
//如果出了循环,就代表fast == null && fast.next == null.链表无环
return false;
}
//17.给定一个链表,返回链表开始入环的第一个节点,如果链表无环,则返回null
//解题方法:这个题目本质上更像是一道数学题.
//设从起点到入口点的距离为X,快慢指针相遇点到入口点的长度为Y,设环的长度为C
//经过数学推算我们可以得到X == Y
public ListNode detectCycle(ListNode head){
if(head == null) return null;
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
break;
}
}
if(fast == null || fast.next == null){
return null;
}
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
//18.判断链表的是否属于回文结构,要求时间复杂度为o(n),空间复杂度为o(1)
//首先找到链表的中间节点,将中间节点之后的节点进行反转后形成一个新的链表
//然后前后两个链表进行比较即可
public boolean chkPalindrom(ListNode head){
if(head == null){
return true;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
//slow走到了中间位置
//然后将链表后半部分进行反转
ListNode cur = slow.next;
while(cur != null){
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//反转完成
while(head != slow){
if(head.val != slow.val){
return false;
}
if(head.next == slow){
return true;
}
head = head.next;
slow = slow.next;
}
return true;
}
//19.输入两个链表找出他们的公共节点:给你两个单链表的头结点headA和headB,请你找出并返回两个单链表相交的起始节点
//问题:1.如果两个链表相交,那么是Y还是X的形状
// 2.如果两个链表相交,值val域相同还是next域相同
//答:Y字型,next域相同
//解题思路: 如果两个链表没有交点,返回null,如果两个链表的长度不一样,先让最长的链表走他们的差值步,然后再同时走,直到相遇
public static ListNode getIntersectionNode(ListNode headA,ListNode headB){
if(headA == null || headB == null){
return null;
}
ListNode pl = headA;
ListNode ps = headB;
//求长度
int lenA = 0;
int lenB = 0;
while(pl != null){
lenA++;
pl = pl.next;
}
pl = headA;
while(ps != null){
lenB++;
ps = ps.next;
}
ps = headB;
int len = lenA - lenB;//差值步
if(len < 0){
pl = headB;
ps = headA;
len = lenB - lenA;
}
//1.pl永远指向最长的链表,ps永远指向最短的链表 2.求到了差值len步
//pl走差值len步 //同时走,直到相遇
//如果一直不相遇怎么办?
while(len != 0){
pl = pl.next;
len--;
}
while(pl!= ps){
ps = ps.next;
pl = pl.next;
}
return pl;//即使是空也返回null不用再if else
}
}
4.双向链表
class ListNode{
public int val;
public ListNode next;
public ListNode prev;
public ListNode(int val){
this.val = val;
}
}
public class MyLinkedList {
public ListNode head;//指向双向链表的头结点
public ListNode last;//指向尾结点
//1.打印双向链表
public void display(){
ListNode cur = head;
while(cur != null){
System.out.print(cur.val +" ");
cur = cur.next;
}
System.out.println();
}
//2.得到链表的长度
public int size(){
int count = 0;
ListNode cur = this.head;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
//3.查找关键字key是否在链表当中
public boolean contains(int key){
ListNode cur = head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//4.头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(this.head == null){
this.head = node;
this.last = node;
}else{
node.next = this.head;
head.prev = node;
this.head = node;
}
}
//5.尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(this.head == null){
this.head = node;
this.last = node;
}else{
last.next = node;
node.prev = last;
this.last = node;
}
}
//6.删除第一次出现关键字为key的节点
//单链表的删除逻辑:必须得找到需要删除的关键字的前驱
//双向链表:当要删除key的时候直接走到这个节点就可以了
//中间位置cur.prev.next = cur.next;cur.next.pre = cur.prev;
//如果删除头结点,或者尾结点,会出现头指针异常
//如果删除尾结点:cur.prev.next = cur.next == null;last = last.prev
//如果删除的是头结点:head = head.next;head.pre = null;
public void removeKey(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
if (cur == head) {
head = head.next;
if(head != null){
head.prev = null;//只有一个节点,而且要被删除
}else{
last = null;
}
} else {
cur.prev.next = cur.next;
if (cur.next != null) {
//中间位置
cur.next.prev = cur.prev;
break;
} else {
last = last.prev;
}
}
return;//删完第一个就走
}
cur = cur.next;
}
}
//7.删除所有值为key的节点
public void removeAllKey(int key) {
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
if (cur == head) {
head = head.next;
if(head != null){
head.prev = null;//只有一个节点,而且要被删除
}else{
last = null;
}
} else {
cur.prev.next = cur.next;
if (cur.next != null) {
//中间位置
cur.next.prev = cur.prev;
break;
} else {
last = last.prev;
}
}
}
cur = cur.next;
}
}
public ListNode searchIndex(int index){
ListNode cur = this.head;
while(index != 0){
cur = cur.next;
index--;
}
/* for (int i = 0; i < index; i++) {
cur = cur.next;
}*/
return cur;//此时cur就是index位置的一个节点
}
//8.任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
ListNode node = new ListNode(data);
if(index < 0 || index > size()){
System.out.println("index位置不合法!");
return;
}
if(index == 0) {
addFirst(data);
return;
}
if(index == size()){
addLast(data);
return ;
}
ListNode cur = searchIndex(index);
node.next = cur.prev.next;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
//9.清空双链表
public void clear(){
while(head != null){
ListNode cur = head.next;
head.next = null;
head.prev = null;
head = cur;
}
last = null;
}
}