22

I want to write a Prolog program to delete the middle element from an odd-numbered list into another list.

For example, If we give : delete_mid([1,2,3,4,5],L) then it will produce : L = [1,2,4,5] as answer.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
Dev
  • 576
  • 3
  • 14
  • 3
    Promise: will put a bounty on this for the best terminating ISO Prolog definition (that is, no coroutining) that at least terminates (universally) in addition to OP's use case also for `?- delete_middle(Ls, []).` and `?- dif(A,B), delete_middle([A|_],[B|_]).` – false Nov 05 '20 at 10:25

11 Answers11

10

I am surprised and a bit saddened that neither answer so far takes the most obvious approach; surely you've heard about it in school (and I suspect it might be what OP is expected to do).

It is however a bit difficult to explain or do at once, so first, here is a predicate to find the middle element:

list_mid([H|T], Mid) :-
    list_mid_1(T, T, H, Mid).

list_mid_1([], _, Mid, Mid).
list_mid_1([_,_|Fast], [S|Slow], _, Mid) :-
    list_mid_1(Fast, Slow, S, Mid).

I hope the names are obvious.

?- list_mid([], Mid).
false.

?- list_mid([x], Mid).
Mid = x.

?- list_mid([a,x,b], Mid).
Mid = x.

?- list_mid([a,a,x,b,b], Mid).
Mid = x.

?- list_mid([a,a,x,b], Mid).
false.

Seems to work. Now, I can try and add the part where it keeps what it throws away at the moment.


I was busy so this took a while. In the meantime, the answer by Raubsauger is exactly what I had in mind. I did not see it and instead wrote this:

delete_mid([H|T], L) :-
    delete_mid_1(T, T, H, L).

delete_mid_1([], Rest, _, Rest).
delete_mid_1([_,_|Fast], [H|Slow], Prev, [Prev|Back]) :-
    delete_mid_1(Fast, Slow, H, Back).

It is not as neat as the solution by Raubsauger but it seems it is otherwise the same solution. It terminates for the test cases by @false.


I thought the list_middle/2 predicate was enough; I am again surprised and a bit saddened that only Raubsauger saw it (or already knew that about it).


Und täglich grüßt das Murmeltier

TA_intern
  • 2,222
  • 4
  • 12
  • 1
    Is the first argument of `find_mid/2` really a find? – false Nov 05 '20 at 10:02
  • I was just posting it. :-) – rajashekar Nov 05 '20 at 10:03
  • 1
    @false no, it is not, it is obviously supposed to be `list_middle/2`. – TA_intern Nov 05 '20 at 10:14
  • 1
    Ah, I was looking for a solution like that. (Also, school was when Bush The Elder was prez, and this one wasn't in Dijkstra's "A Discipline of Programming") – David Tonhofer Nov 05 '20 at 10:17
  • @DavidTonhofer I suspect that it was in textbooks at that time already. See [here](https://en.wikipedia.org/wiki/Cycle_detection#Algorithms). – TA_intern Nov 06 '20 at 07:46
  • I am saddened by this leftover choicepoint when the first argument is a partial list only. And then, wouldn't it be nice to somehow make the programming idea behind more explicit (ok, that would probably not fit into a single answer, yet)? – false Nov 15 '20 at 21:20
  • @false which choice point exactly? This one: `?- delete_mid(L, []). L = [_29520] ; false.` ? – TA_intern Nov 16 '20 at 07:30
  • @TA_intern: This one *could* be solved by a better indexing. It corresponds to `append(Xs,Ys,[])`. But `delete_mid(L,[a,b])` cannot be solved by indexing alone. – false Nov 16 '20 at 13:28
8

And now I want to join too (answer no. 8 to this question).

delete_mid(Ori, Del):-
    delete_mid(Ori, Ori, Del).

delete_mid([_], [_|Slow], Slow).
delete_mid([_,_|Fast], [H|Slow], [H|Ret]):-    
    delete_mid(Fast, Slow, Ret).

?- delete_mid([1, 2, 3, 4, 5], Del).
Del = [1, 2, 4, 5] ;
false.

?- delete_mid([1, 2, 3, 4], Del).
false.

?- delete_mid(L, []).
L = [_1500] ;
false.

?- dif(A,B), delete_mid([A|_], [B|_]).
false.

To the idea: I saw TA_interns answer about getting the middle element (list_mid) and thought:
This is genius. But wait ... this can be improved.


To explain the algorithm a bit further: the predicate can be used to generate a list which is similar to the (odd numbered) input list without middle element. Or it can test for two lists if this property holds.

The "genius" part is that there is no need to calculate the length or have counters because it actually uses a copy of the input list as counter. The principle is explained here and here.

Lines 1 & 2 create two references to the same list. The counter list is called fast, the element list is called slow. Why? Because in each recursion step you strip two elements from the fast list ([_,_|Fast]) but only one from the element list ([H|Slow]). When there is exactly one element in the fast list left ([_]) you hit the middle element from the slow list. So remove it and put the rest on the return track. While going forwards with the recursion put all the elements (H) which you removed from the slow list as head of the return list, and the recursion fills in the rest.

Et voilà you got an exact copy of the element list, just the middle element is missing.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
DuDa
  • 3,718
  • 4
  • 16
  • 36
  • 2
    Best solution so far. – TA_intern Nov 06 '20 at 07:44
  • 1
    I did not want to give a working solution so fast. I thought it isn't necessary. You are the only person who picked up on it. Credit for the idea goes to [no one in particular](https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare). Your final solution is maybe better than what I came up with. – TA_intern Nov 06 '20 at 09:08
  • right, except, not a copy, but the original list itself, with two *pointers* into it, one at twice the speed of the other. and there's no going *backwards* through this recursion, because it's *tail* recursion. the prepending `[H|Ret]` in the rule's head is done *before* the recursive call, which fills the hole, `Ret`. see [1](https://stackoverflow.com/tags/tailrecursion-modulo-cons/info). – Will Ness Nov 16 '20 at 06:18
  • oh no, the whole edit, why? it was a minor point only... – Will Ness Nov 16 '20 at 08:45
  • 1
    I took the liberty to restore it, with some minor alterations... :) – Will Ness Nov 16 '20 at 08:50
5

I think you need the nth0/4 predicate. Just find the index of the middle element and then remove it using nth0/4.

delete_middle(Ls, Ls1) :-
    length(Ls, L),
    divmod(L, 2, Q, 1),   % constrain remainder to be 1: fails on even list
    nth0(Q, Ls, _, Ls1).

Generative Variant: The only problem was with divmod.

divmod1(Dividend, Divisor, Quotient, Remainder) :-
    (   var(Dividend)
    ->  Dividend is Divisor*Quotient+Remainder
    ;   divmod(Dividend, Divisor, Quotient, Remainder)
    ).

delete_middle(Ls, Ls1) :- % Reversed the clauses.
    nth0(Q, Ls, _, Ls1),
    divmod1(L, 2, Q, 1),
    length(Ls, L).
?- dif(A, B), delete_middle([A|_], [B|_]).
false.

?- delete_middle(X, []).
X = [_382] ;
false.
false
  • 10,264
  • 13
  • 101
  • 209
rajashekar
  • 3,460
  • 11
  • 27
  • 1
    and what about lists with even number of elements? – DuDa Nov 05 '20 at 07:37
  • 1
    @Raubsauger : The question was constrained to `odd-numbered list`. In lists of even length what do you define as a middle element? Either ignore deletion or maybe delete two of them. – rajashekar Nov 05 '20 at 07:43
  • wha, thanks for claering this, next time I will read more carefully – DuDa Nov 05 '20 at 07:46
  • This will work also on a list with even number of elements. Maybe you could use the last argument of divmod for this. But divmod is not universally available I guess? Not that it matters, really. – TA_intern Nov 05 '20 at 09:11
  • 2
    @DavidTonhofer If the length is not already constrained to be odd, then setting the remainder in divmod to 1 will solve the problem. – rajashekar Nov 05 '20 at 09:26
  • `?- delete_middle(Ls, []).` does not terminate. – false Nov 05 '20 at 10:06
  • Nor does `?- dif(A,B), delete_middle([A|_],[B|_]).` – false Nov 05 '20 at 10:11
  • @false : Isn't delete_middle(Ls, []) returning a singleton list first? – rajashekar Nov 05 '20 at 10:11
  • @rajashekar, yes it does find a solution, but it loops thereafter. – false Nov 05 '20 at 10:13
  • @false `dif(A,B), delete_middle([A|_],[B|_]).` is rather nasty. Why would you think of that! – David Tonhofer Nov 05 '20 at 10:22
  • @DavidTonhofer: There are even nastier cases to consider. – false Nov 05 '20 at 10:27
  • 1
    @false: re-order the clauses. Put nth0 predicate first then it terminates. – rajashekar Nov 05 '20 at 10:29
  • 1
    @false That's the problem with Prolog - You never know what will be thrown at your predicate, but you are also loath to add a bevvy of must_be's to restrict to use to actually known-to-work cases. – David Tonhofer Nov 05 '20 at 10:29
  • 1
    @DavidTonhofer: you can see this as a problem. But at the same time you can see this as one of the advantages: that you can produce diagnostics that cover much larger sets of queries. – false Nov 05 '20 at 10:32
  • 1
    Use `listing(divmod1)` to see how to indent this properly. – false Nov 07 '20 at 11:13
  • @false : Thank you. Is this a standard way to check indentation, formatting? – rajashekar Nov 07 '20 at 12:00
  • @rajashekar: *Is this a standard way to check indentation* it works like this in SWI. It is not required by the standard though. – false Nov 07 '20 at 12:39
4

The solution with nth0/4 is efficient but how about we solve this declaratively?

middle_less(InList,MiddlelessList,Middle) :-
   append([Prefix,[Middle],Suffix],InList),
   length(Prefix,Len),
   length(Suffix,Len),
   append(Prefix,Suffix,MiddlelessList).

Which is basically the problem statement in Prolog form.

It also works:

:- begin_tests(middleless).

test("empty list",fail) :- middle_less([],_,_).

test("1-element list",[true([MLL,M] == [[],a]),nondet]) :-
   middle_less([a],MLL,M).

test("2-element list",fail) :- 
   middle_less([a,b],_,_).

test("3-element list",[true([MLL,M] == [[a,c],b]),nondet]) :-
   middle_less([a,b,c],MLL,M).

:- end_tests(middleless).

And so:

?- run_tests.
% PL-Unit: middleless .... done
% All 4 tests passed
true.

But with a list of 1001 elements:

?- length(L,1001),time(middle_less(L,MLL,M)).
% 757,517 inferences, 0.110 CPU in 0.111 seconds (99% CPU, 6862844 Lips)

One day, the compiler with morph the spec of middle_less automagically into an efficent solution.

David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
  • 1
    Moving the two length/2 into same__length/2 as first goal improves the timings 5x, but the termination problem posted by @false cannot be solved in this way. Giving up... – CapelliC Nov 07 '20 at 06:51
  • 1
    How about: `middle_less(InList,MiddlelessList,Middle) :- list_onelesslong(InList, MiddlelessList), ` with `list_onelesslong([_], []). list_onelesslong([_X | Xs], [_Y | Ys]) :- list_onelesslong(Xs, Ys).`. This allows the predicate to generate answers. – Isabelle Newbie Nov 10 '20 at 16:27
3
delete_middle([], [], _MiddleDeletedPrefix) -->
    [_Middle].
delete_middle([L | Left], [R | ReversedRight], [L | MiddleDeletedPrefix]) -->
    [L],
    delete_middle(Left, ReversedRight, MiddleDeletedPrefix),
    [R].

delete_middle(List, MiddleDeleted) :-
    phrase(delete_middle(Left, ReversedRight, MiddleDeleted), List),
    reverse(ReversedRight, Right),
    append(Left, Right, MiddleDeleted).

 

?- delete_middle([1, 2, 3, 4, 5], Xs).
Xs = [1, 2, 4, 5] ;
false.

?- delete_middle(Ls, []).
Ls = [_2542] ;
false.

?- dif(A,B), delete_middle([A|_],[B|_]).
false.

?- delete_middle(List, MiddleDeleted).
List = [_2368],
MiddleDeleted = [] ;
List = [_2368, _2392, _2374],
MiddleDeleted = [_2368, _2374] ;
List = [_2368, _2392, _2416, _2398, _2374],
MiddleDeleted = [_2368, _2392, _2398, _2374] ;
List = [_2368, _2392, _2416, _2440, _2422, _2398, _2374],
MiddleDeleted = [_2368, _2392, _2416, _2422, _2398, _2374] ;
List = [_2368, _2392, _2416, _2440, _2464, _2446, _2422, _2398, _2374],
MiddleDeleted = [_2368, _2392, _2416, _2440, _2446, _2422, _2398, _2374] .  % etc.
Isabelle Newbie
  • 9,258
  • 1
  • 20
  • 32
2

New version, now even more deterministic:

delete_mid(List, MiddleDeleted) :-
    List = [_ | Tail],
    gallop(Tail, MiddleDeleted, List, MiddleDeleted).

gallop([], [], [_Middle | Xs], Xs).
gallop([_,_ | Fast1], [_,_ | Fast2], [X | Xs], [X | Ys]) :-
    gallop(Fast1, Fast2, Xs, Ys).

What's new versus previous answers is that this runs down both lists at double speed, while also copying the prefix at the same time. It needs shallow indexing on at least the first two arguments to be deterministic, but SWI-Prolog does that:

?- delete_mid([1, 2, 3, 4, 5], MiddleDeleted).
MiddleDeleted = [1, 2, 4, 5].

?- delete_mid(Xs, []).
Xs = [_2008].

?- delete_mid(Xs, [a, b]).
Xs = [a, _2034, b].

?- dif(A, B), delete_mid([A | _], [B | _]).
false.
Isabelle Newbie
  • 9,258
  • 1
  • 20
  • 32
1

Building upon the find the middle algorithm presented by TA_intern :

%! list_without_middle(SOURCEs,TARGETs)

list_without_middle(SOURCEs,TARGETs)
:-
list_middle(SOURCEs,_MIDDLE_,PREFIXs,SUFFIXs) ,
lists:append(PREFIXs,SUFFIXs,TARGETs)
.

%!  list_middle(LISTs,MIDDLE,PREFIXs,SUFFIXs)

list_middle([ITEM|LISTs],MIDDLE,PREFIXs,SUFFIXs)
:-
list_middle(LISTs,LISTs,ITEM,MIDDLE,PREFIXs,SUFFIXs)
.

%!  list_middle(FASTs,SLOWs,ITEM,MIDDLE,PREFIXs,SUFFIXs)

list_middle([],SLOWs,ITEM,ITEM,[],SLOWs) .

list_middle([_,_|FASTs],[ITEM|SLOWs],PREVIOUS_ITEM,MIDDLE,[PREVIOUS_ITEM|PREFIXs],SUFFIXs)
:-
list_middle(FASTs,SLOWs,ITEM,MIDDLE,PREFIXs,SUFFIXs)
.
?- list_without_middle([a,b,c],Ys).
Ys = [a, c].

?- list_without_middle([a,c],Ys).
false.

?- list_without_middle([a,b,c,d,e],Ys).
Ys = [a, b, d, e].

?- 
?- list_without_middle(Xs,Ys) .
Xs = [_924],
Ys = [] ;
Xs = [_924, _930, _936],
Ys = [_924, _936] ;
Xs = [_924, _930, _936, _948, _954],
Ys = [_924, _930, _948, _954] %.e.t.c.
?- list_middle([a,b,c],MIDDLE,PREFIXs,SUFFIXs).
MIDDLE = b,
PREFIXs = [a],
SUFFIXs = [c].

?- list_middle([a,c],MIDDLE,PREFIXs,SUFFIXs).
false.

?- list_middle([a,b,c,d,e],MIDDLE,PREFIXs,SUFFIXs).
MIDDLE = c,
PREFIXs = [a, b],
SUFFIXs = [d, e].

?- 
?- list_without_middle(Ls,[]) .
Ls = [_4364] ;
ERROR: Out of global stack
?- list_without_middle([a],Ys).
Ys = [].

?- dif(A,B) , list_without_middle([A|_],[B|_]) .
ERROR: Out of global stack
?- 
Kintalken
  • 763
  • 5
  • 9
1

This solution keeps a counter to unify the tail with a proper length list after "taking out" the middle item:

without_middle(Ls, Ls1):-
  without_middle(Ls, 0, Ls1).
  
without_middle([_Mid|Tail], Len, Tail):-
  length(Tail, Len).
without_middle([Item|Tail], Len, [Item|NTail]):-
  succ(Len, Len1),
  without_middle(Tail, Len1, NTail).

This slight variation embeds the counting+length+unification of the second half more directly, yielding a better performance results for large lists:

without_middle(Ls, Ls1):-
  without_middle(Ls, [], Ls1).

without_middle([_Mid|Tail], Tail, Tail).
without_middle([Item|Tail], RTail, [Item|NTail]):-
   without_middle(Tail, [_|RTail], NTail).

Sample test cases:

?- without_middle([a,b,c,d,e,f,g], L).
L = [a, b, c, e, f, g] ;
false.

?- without_middle([a,b,c,d,e,f], L).
false.

?- without_middle(L, []).
L = [_552] ;
false.

?- dif(A,B), without_middle([A|_], [B|_]).
false.
gusbro
  • 22,357
  • 35
  • 46
1

Making use of append/3:

del_mid([_], []).         % if input only has one element => output is []
del_mid([H|T], [H|X]) :- 
  append(M, [Litem], T),  % M = list without first and last (Litem) element
  del_mid(M, R),          % Apply on M; if M is only one item => R will be []
  append(R, [Litem], X).  % X = R + [last item] => which gets added as result's tail

Some examples:

?- del_mid([], X).
false.

?- del_mid([a], X).
X = [] ;
false.

?- del_mid([a,b], X).
false.

?- del_mid([a,b,c], X).
X = [a, c] ;
false.

?- del_mid([a,b,c,d,e,f,g], X).
X = [a, b, c, e, f, g] ;
false.
tokosh
  • 1,772
  • 3
  • 20
  • 37
0

Not a straight forward nor a more optimal answer.

delete_middle1(Ls, Ls1) :- delete_middle1_(Ls, Ls, [], Ls1).
delete_middle1_([X | Cs], [_, _ | Ds], Acc, L) :-
    delete_middle1_(Cs, Ds, [X | Acc], L).
delete_middle1_([_ | Cs], [_], Acc, L) :-  revappend(Acc, Cs, L).

revappend([], L, L).
revappend([X | L1], L2, L3) :- revappend(L1, [X | L2], L3).

This method works well when dealing with linked-lists and pointers. When one pointer is at the end, the other will be near the middle. Then we can just delete the element.

rajashekar
  • 3,460
  • 11
  • 27
0

Here is my prolog solution:

delMidNumber(K,L):-
     len(K,N),
     (N mod 2 =:= 1 ->  
      N1 is N//2,
      nth0(N1,K,E1),
      del(E1,K,L); write('List has even length'),!).

len([],0).
len([H|T],N):-
    len(T,N1),
    N is N1+1.

del(E,[E|T],T).
del(E,[H|T],[H|T1]):-
    del(E,T,T1).

The predicate delMidNumber takes two arguments 1-The List with odd numbers. 2- The New List that will be formed. The predicate first calculates the length of the list, it then checks if the length of the list is odd then it divides the length by 2. The result is then used in nth0 to give us the element on that index. We then simply use the the del predicate to delete that mid number element. If the length is even it writes the message that the length is even and then cuts (stops).

?-delMidNumber([1,3,2,4,5],L).
L = [1, 3, 4, 5]

?-delMidNumber([1,3,4,5],L).
List has even length
Reema Q Khan
  • 878
  • 1
  • 7
  • 20
  • I like that you are putting effort into learning prolog. Could you add another test case to yours? Try `delMidNumber([2,3,2,4,5],L).` and you might find something to improve. – DuDa Nov 14 '20 at 21:29
  • @Raubsauger Yes I tried it out. It is returning L=[3,2,4,5] and the final expected answer L=[2,3,4,5]. – Reema Q Khan Nov 15 '20 at 10:36